def display_multiHex(rings_number_mother, sec_rad_mother, side_dist_mother = 1.0,segment_gap_mother = 0.01, pixel_scale = 0.010, fov_arcsec_mag = 2.0, figure_size = (12 ,8),wvl = 1e-6): def multi_hexagon(rings_number, sec_rad, side_dist = 1.0, segment_gap = 0.01): """ multi_hexagon(rings_number, sec_rad, side_dist = 1.0, segment_gap = 0.01) # rings : The number of rings of hexagons to include, not counting the central segment # side_dist : Distance between sides (flat-to-flat) of the hexagon, in meters. Default is 1.0 # segment_gap : Gap between adjacent segments, in meters. Default is 0.01 m = 1 cm # sec_rad : scondary obstacle radius """ ap = poppy.MultiHexagonAperture(name='ApertureHex', flattoflat = side_dist, gap = segment_gap,rings =rings_number) # 3 rings of 2 m segments yields 14.1 m circumscribed diameter sec = poppy.SecondaryObscuration(secondary_radius = float(sec_rad), n_supports = 4, support_width = 0.1) # secondary with spiders atlast = poppy.CompoundAnalyticOptic( opticslist=[ap, sec], name='Mock ATLAST') # combine into one optic plt.figure(figsize=(12,8)) atlast.display(npix=1024, colorbar_orientation='vertical') return atlast osys = poppy.OpticalSystem() osys.add_pupil(multi_hexagon(rings_number_mother,sec_rad_mother,side_dist_mother,segment_gap_mother)) osys.add_detector(pixelscale=pixel_scale, fov_arcsec=fov_arcsec_mag) psf = osys.calc_psf(wvl) plt.figure(figsize=figure_size) poppy.display_psf(psf, title="Diffraction Pattern")
def rectangle_intensity(rot=0, wd=2, ht=1, xshi=0, yshi=0): """Calculate light intensity for light going through a rectangular slit rectangle_intensity(rot = 0, wd= 2,ht = 1, xshi = 0,yshi = 0) rot : rotation in degrees wd : width in m ht : height in m xshi : x axis shift in m yshi : y axis shift in m """ ap = poppy.RectangleAperture(rotation=rot, width=wd, height=ht, shift_x=xshi, shift_y=yshi) ap.display(colorbar=False) osys = poppy.OpticalSystem() osys.add_pupil(ap) osys.add_detector(pixelscale=0.05, fov_arcsec=2.0) psf = osys.calc_psf(1e-6) plt.figure(figsize=(12, 8)) poppy.display_psf(psf, title="Diffraction Pattern")
def test_rotation_in_OpticalSystem(display=False, npix=1024): """ Test that rotation planes within an OpticalSystem work as expected to rotate the wavefront. We can get equivalent results by rotating an Optic a given amount counterclockwise, or rotating the wavefront in the same direction. """ angles_and_tolerances = ((90, 1e-8), (45, 3e-7)) for angle, atol in angles_and_tolerances: osys = poppy.OpticalSystem(npix=npix) osys.add_pupil(poppy.optics.ParityTestAperture(rotation=angle)) osys.add_detector(fov_pixels=128, pixelscale=0.01) psf1 = osys.calc_psf() osys2 = poppy.OpticalSystem(npix=npix) osys2.add_pupil(poppy.optics.ParityTestAperture()) osys2.add_rotation(angle=angle) # note, same sign here. osys2.add_detector(fov_pixels=128, pixelscale=0.01) psf2 = osys2.calc_psf() if display: fig, axes = plt.subplots(figsize=(16, 5), ncols=2) poppy.display_psf(psf1, ax=axes[0]) axes[0].set_title("Optic rotated {} deg".format(angle)) poppy.display_psf(psf2, ax=axes[1]) axes[1].set_title("Wavefront rotated {} deg".format(angle)) assert np.allclose(psf1[0].data, psf2[0].data, atol=atol), ( "PSFs did not agree " f"within the requested tolerance, for angle={angle}." f"Max |difference| = {np.max(np.abs(psf1[0].data - psf2[0].data))}" )
def test_MFT_FFT_equivalence_in_OpticalSystem(tmpdir, display=False, source_offset=1): """ Test that propagating Wavefronts through an OpticalSystem using an MFT and an FFT give equivalent results. This is a somewhat higher level test that involves all the Wavefront class's _propagateTo() machinery, which is not tested in the above function. Hence the two closely related tests. This test now includes a source offset, to test equivalence of handling for nonzero WFE, in this case for tilts. """ # Note that the Detector class and Wavefront propagation always uses # ADJUSTABLE-style MFTs (output centered in the array) # which is not compatible with FFT outputs for even-sized arrays. # Thus in order to get an exact equivalence, we have to set up our # OpticalSystem so that it, very unusually, uses an odd size for # its input wavefront. The easiest way to do this is to discretize # an AnalyticOpticalElement onto a specific grid. fn = str(tmpdir / "test.fits") fits511 = optics.ParityTestAperture().to_fits(fn, wavelength=1e-6, npix=511) pup511 = poppy_core.FITSOpticalElement(transmission=fits511) # set up simple optical system that will just FFT fftsys = poppy_core.OpticalSystem(oversample=1) fftsys.add_pupil(pup511) fftsys.add_image() fftsys.source_offset_r = source_offset fftsys.source_offset_theta = 90 fftpsf, fftplanes = fftsys.calc_psf(display=False, return_intermediates=True) # set up equivalent using an MFT, tuned to get the exact same scale # for the image plane mftsys = poppy_core.OpticalSystem(oversample=1) mftsys.add_pupil(pup511) mftsys.add_detector(pixelscale=fftplanes[1].pixelscale , fov_pixels=fftplanes[1].shape, oversample=1) #, offset=(pixscale/2, pixscale/2)) mftsys.source_offset_r = source_offset mftsys.source_offset_theta = 90 mftpsf, mftplanes = mftsys.calc_psf(display=False, return_intermediates=True) if display: import poppy plt.figure(figsize=(15,4)) plt.subplot(131) poppy.display_psf(fftpsf, title="FFT PSF") plt.subplot(132) poppy.display_psf(mftpsf, title='MFT PSF') plt.subplot(133) poppy.display_psf_difference(fftpsf, mftpsf, title='Diff FFT-MFT') assert( np.all( np.abs(mftpsf[0].data-fftpsf[0].data) < 1e-10 ))
def makePSF(self, makePSFInputDict: dict, makePSFOptions: zernikeoptions): coeffs = makePSFInputDict["coeffs"] show = makePSFOptions["show"] units = makePSFOptions["units"] extraPlots = makePSFOptions["extraPlots"] if units is "microns": coeffs = np.asarray(coeffs) * 1e-6 osys = poppy.OpticalSystem() circular_aperture = poppy.CircularAperture(radius=self.radius) osys.add_pupil(circular_aperture) thinlens = poppy.ZernikeWFE(radius=self.radius, coefficients=coeffs) osys.add_pupil(thinlens) # osys.add_detector(pixelscale=self.pixscale, fov_arcsec=self.FOV) osys.add_detector(pixelscale=self.pixscale, fov_pixels=self.FOV_pixels) if extraPlots: plt.figure(1) # psf_with_zernikewfe, final_wf = osys.calc_psf(wavelength=self.wavelength, display_intermediates=show, # return_final=True) psf_with_zernikewfe, all_wfs = osys.calc_psf( wavelength=self.wavelength, display_intermediates=show, return_intermediates=True, ) final_wf = all_wfs[-1] pupil_wf = all_wfs[1] if extraPlots: psf = psf_with_zernikewfe psfImage = psf[0].data # plt.figure(2) # plt.clf() # poppy.display_psf(psf, normalize='peak', cmap='viridis', scale='linear', vmin=0, vmax=1) plt.figure(3) wf = final_wf wf = pupil_wf plt.clf() plt.pause(0.001) plt.subplot(1, 2, 1) plt.imshow(wf.amplitude**2) plt.title("Amplitude ^2") plt.colorbar() plt.subplot(1, 2, 2) plt.imshow(wf.phase) plt.title("Phase") plt.colorbar() plt.tight_layout() poppy.display_psf(psf_with_zernikewfe, title="PSF") self.psf = psf_with_zernikewfe self.wf = final_wf self.complex_psf = self.wf.amplitude * np.exp(1j * self.wf.phase) self.osys_obj = osys self.pupil_wf = pupil_wf
def test_radial_profile_of_offset_source(): """Test that we can compute the radial profile for a source slightly outside the FOV, compare that to a calculation for a centered source, and check we get consistent results for the overlapping range of the radius parameter space. Also, make a plot showing the consistency. """ import matplotlib.pyplot as plt osys = poppy.OpticalSystem() osys.add_pupil(poppy.CircularAperture(radius=1.0)) osys.add_detector(pixelscale=0.01, fov_pixels=128) # compute centered PSF psf0 = osys.calc_psf() # Compute a PSF with the source offset osys.source_offset_r = 1.0 # outside of FOV psf1 = osys.calc_psf() # Calculate the radial profiles of those two PSFs r0, p0 = poppy.radial_profile(psf0) # For the offset PSF, compute apparent coordinates of the offset source in that image # (this will be a 'virtual' pixel value outside of the FOV) halfsize = psf1[0].header['NAXIS1'] // 2 offset_ypos_in_pixels = osys.source_offset_r / psf1[0].header[ 'PIXELSCL'] + halfsize offset_target_center_pixels = (halfsize, offset_ypos_in_pixels) r1, p1 = poppy.radial_profile(psf1, center=offset_target_center_pixels) fig, axes = plt.subplots(figsize=(16, 5), ncols=3) poppy.display_psf(psf0, ax=axes[0], title='Centered', colorbar_orientation='horizontal') poppy.display_psf(psf1, ax=axes[1], title='Offset', colorbar_orientation='horizontal') axes[2].semilogy(r0, p0) axes[2].semilogy(r1, p1) # Measure radial profiles as interpolator objects, so we can evaluate them at identical radii prof0 = poppy.measure_radial(psf0) prof1 = poppy.measure_radial(psf1, center=offset_target_center_pixels) # Test consistency of those two radial profiles at various radii within the overlap region test_radii = np.linspace(0.4, 0.8, 7) for rad in test_radii: print(prof0(rad), prof1(rad)) axes[2].axvline(rad, ls=':', color='black') # Check PSF agreement within 10%; # also add an absolute tolerance since relative error can be higher for radii right on the dark Airy nuls assert np.allclose( prof0(rad), prof1(rad), rtol=0.1, atol=5e-8 ), "Disagreement between centered and offset radial profiles"
def makeZernikePSF(self, coeffs=(0, 0, 0, 0, 0), show=False, units='microns', extraPlots=False): # RADIUS = 1.0 # meters # WAVELENGTH = 1500e-9 # meters # PIXSCALE = 0.01 # arcsec / pix # FOV = 1 # arcsec # NWAVES = 1.0 # FOV_PIXELS = 128 if units == 'microns': coeffs = np.asarray(coeffs) * 1e-6 osys = poppy.OpticalSystem() circular_aperture = poppy.CircularAperture(radius=self.radius) osys.add_pupil(circular_aperture) thinlens = poppy.ZernikeWFE(radius=self.radius, coefficients=coeffs) osys.add_pupil(thinlens) #osys.add_detector(pixelscale=self.pixscale, fov_arcsec=self.FOV) osys.add_detector(pixelscale=self.pixscale, fov_pixels=self.FOV_pixels) if extraPlots: plt.figure(1) # psf_with_zernikewfe, final_wf = osys.calc_psf(wavelength=self.wavelength, display_intermediates=show, # return_final=True) psf_with_zernikewfe, all_wfs = osys.calc_psf(wavelength=self.wavelength, display_intermediates=show, return_intermediates=True) final_wf = all_wfs[-1] pupil_wf = all_wfs[1] #this one *** if extraPlots: psf = psf_with_zernikewfe psfImage = psf[0].data plt.figure(2) plt.clf() poppy.display_psf(psf, normalize='peak', cmap='viridis', scale='linear', vmin=0, vmax=1) plt.pause(0.001) plt.figure(3) wf = final_wf wf = pupil_wf plt.clf() plt.pause(0.001) plt.subplot(1, 2, 1) plt.imshow(wf.amplitude ** 2) plt.title('Amplitude ^2') plt.colorbar() plt.subplot(1, 2, 2) plt.imshow(wf.phase) plt.title('Phase') plt.colorbar() plt.tight_layout() self.psf = psf_with_zernikewfe self.wf = final_wf self.osys_obj = osys self.pupil_wf = pupil_wf
def test_MatrixFT_FFT_Lyot_propagation_equivalence(display=False): """ Using a simple Lyot coronagraph prescription, perform a simple numerical check for consistency between calc_psf result of standard (FFT) propagation and MatrixFTCoronagraph subclass of OpticalSystem.""" D = 2. wavelen = 1e-6 ovsamp = 4 fftcoron_annFPM_osys = poppy.OpticalSystem(oversample=ovsamp) fftcoron_annFPM_osys.add_pupil(poppy.CircularAperture(radius=D / 2)) spot = poppy.CircularOcculter(radius=0.4) diaphragm = poppy.InverseTransmission(poppy.CircularOcculter(radius=1.2)) annFPM = poppy.CompoundAnalyticOptic(opticslist=[diaphragm, spot]) fftcoron_annFPM_osys.add_image(annFPM) fftcoron_annFPM_osys.add_pupil(poppy.CircularAperture(radius=0.9 * D / 2)) fftcoron_annFPM_osys.add_detector(pixelscale=0.05, fov_arcsec=3.) # Re-cast as MFT coronagraph with annular diaphragm FPM matrixFTcoron_annFPM_osys = poppy.MatrixFTCoronagraph( fftcoron_annFPM_osys, occulter_box=diaphragm.uninverted_optic.radius_inner) annFPM_fft_psf = fftcoron_annFPM_osys.calc_psf(wavelen) annFPM_mft_psf = matrixFTcoron_annFPM_osys.calc_psf(wavelen) diff_img = annFPM_mft_psf[0].data - annFPM_fft_psf[0].data abs_diff_img = np.abs(diff_img) if display: plt.figure(figsize=(16, 3)) plt.subplot(131) poppy.display_psf(annFPM_fft_psf, vmin=1e-10, vmax=1e-6, title='Annular FPM Lyot coronagraph, FFT') plt.subplot(132) poppy.display_psf(annFPM_mft_psf, vmin=1e-10, vmax=1e-6, title='Annular FPM Lyot coronagraph, Matrix FT') plt.subplot(133) plt.imshow((annFPM_mft_psf[0].data - annFPM_fft_psf[0].data), cmap='gist_heat') plt.colorbar() plt.title('Difference (MatrixFT - FFT)') plt.show() print("Max of absolute difference: %.10g" % np.max(abs_diff_img)) assert (np.all(abs_diff_img < 2e-7))
def test_displays(): # Right now doesn't check the outputs are as expected in any way # TODO consider doing that? But it's hard given variations in matplotlib version etc import poppy import matplotlib.pyplot as plt osys = poppy.OpticalSystem() osys.add_pupil(poppy.CircularAperture()) osys.add_detector(fov_pixels=128, pixelscale=0.01) osys.display() plt.figure() psf = osys.calc_psf(display_intermediates=True) plt.figure() #psf = osys.calc_psf(display_intermediates=True) poppy.display_psf(psf)
def circular_intensity(wvl, pupil_radius, pixel_scale_par=0.009): """ Calculate and plot circular intensity circular_intensity(wvl, pupil_radius, pixel_scale_par = 0.05) wvl : wavelength in microns """ osys = poppy.OpticalSystem() osys.add_pupil( poppy.CircularAperture(radius=pupil_radius)) # pupil radius in meters planeCor = pupil_radius # This line will let us change coordinates of the plane according to the pupil radius to better represent the diffraction pattern if pupil_radius <= 0.49: planeCor = pupil_radius * 10 osys.add_detector( pixelscale=pixel_scale_par, fov_arcsec=planeCor) # image plane coordinates in arcseconds psf = osys.calc_psf(wvl) # wavelength in meters poppy.display_psf(psf, title='The Circular Aperture')
def test_displays(): # Right now doesn't check the outputs are as expected in any way # TODO consider doing that? But it's hard given variations in matplotlib version etc import poppy import matplotlib.pyplot as plt osys = poppy.OpticalSystem() osys.add_pupil(poppy.CircularAperture()) osys.add_detector(fov_pixels=128, pixelscale=0.01) osys.display() plt.figure() psf = osys.calc_psf(display_intermediates=True) plt.figure() #psf = osys.calc_psf(display_intermediates=True) poppy.display_psf(psf)
def test_MatrixFT_FFT_Lyot_propagation_equivalence(display=False): """ Using a simple Lyot coronagraph prescription, perform a simple numerical check for consistency between calc_psf result of standard (FFT) propagation and MatrixFTCoronagraph subclass of OpticalSystem.""" D = 2. wavelen = 1e-6 ovsamp = 4 fftcoron_annFPM_osys = poppy.OpticalSystem( oversample=ovsamp ) fftcoron_annFPM_osys.add_pupil( poppy.CircularAperture(radius=D/2) ) spot = poppy.CircularOcculter( radius=0.4 ) diaphragm = poppy.InverseTransmission( poppy.CircularOcculter( radius=1.2 ) ) annFPM = poppy.CompoundAnalyticOptic( opticslist = [diaphragm, spot] ) fftcoron_annFPM_osys.add_image( annFPM ) fftcoron_annFPM_osys.add_pupil( poppy.CircularAperture(radius=0.9*D/2) ) fftcoron_annFPM_osys.add_detector( pixelscale=0.05, fov_arcsec=3. ) # Re-cast as MFT coronagraph with annular diaphragm FPM matrixFTcoron_annFPM_osys = poppy.MatrixFTCoronagraph( fftcoron_annFPM_osys, occulter_box=diaphragm.uninverted_optic.radius_inner ) annFPM_fft_psf = fftcoron_annFPM_osys.calc_psf(wavelen) annFPM_mft_psf = matrixFTcoron_annFPM_osys.calc_psf(wavelen) diff_img = annFPM_mft_psf[0].data - annFPM_fft_psf[0].data abs_diff_img = np.abs(diff_img) if display: plt.figure(figsize=(16,3)) plt.subplot(131) poppy.display_psf(annFPM_fft_psf, vmin=1e-10, vmax=1e-6, title='Annular FPM Lyot coronagraph, FFT') plt.subplot(132) poppy.display_psf(annFPM_mft_psf, vmin=1e-10, vmax=1e-6, title='Annular FPM Lyot coronagraph, Matrix FT') plt.subplot(133) plt.imshow( (annFPM_mft_psf[0].data - annFPM_fft_psf[0].data), cmap='gist_heat') plt.colorbar() plt.title('Difference (MatrixFT - FFT)') plt.show() print("Max of absolute difference: %.10g" % np.max(abs_diff_img)) assert( np.all(abs_diff_img < 2e-7) )
def test_displays(): # Right now doesn't check the outputs are as expected in any way # TODO consider doing that? But it's hard given variations in matplotlib version etc # As a result this just tests that the code runs to completion, without any assessment # of the correctness of the output displays. import poppy import matplotlib.pyplot as plt osys = poppy.OpticalSystem() osys.add_pupil(poppy.CircularAperture()) osys.add_detector(fov_pixels=128, pixelscale=0.01 * u.arcsec / u.pixel) # Test optical system display # This implicitly exercises the optical element display paths, too osys.display() # Test PSF calculation with intermediate wavefronts plt.figure() psf = osys.calc_psf(display_intermediates=True) # Test PSF display plt.figure() poppy.display_psf(psf) # Test PSF display with other units too poppy.display_psf(psf, angular_coordinate_unit=u.urad) # Test PSF calculation with intermediate wavefronts and other units plt.figure() psf = osys.calc_psf(display_intermediates=True) osys2 = poppy.OpticalSystem() osys2.add_pupil(poppy.CircularAperture()) osys2.add_detector(fov_pixels=128, pixelscale=0.05 * u.urad / u.pixel) psf2, waves = osys.calc_psf(display_intermediates=True, return_intermediates=True) # Test wavefront display, implicitly including other units waves[-1].display() plt.close('all')
fov_pixels=n_pix, distance=fl_obj + delta) # System description just one time if image_idx == 1: print('\nOptical System description:') osys.describe() print('\n') #plt.figure(figsize=(12,8)) psf = osys.calc_psf(wavelength=wavelength, display_intermediates=False, return_intermediates=False) plt.figure(figsize=(10, 10)) poppy.display_psf(psf, normalize='total') plt.title('objective lens PSF_{},'.format(image_idx)) if crop: psf_train = crop_in_center(psf[0].data, crop_size=crop_size) else: psf_train = psf[0].data if normalisation: psf_train = psf_train / np.max(psf_train) if gauss_noise: psf_train = gaussian_noise(psf_train)
def test_radial_profile(plot=False): """ Test radial profile calculation, including circular and square apertures, and including with the pa_range option. """ ### Tests on a circular aperture o = poppy_core.OpticalSystem() o.add_pupil(poppy.CircularAperture(radius=1.0)) o.add_detector(0.010, fov_pixels=512) psf = o.calc_psf() rad, prof = poppy.radial_profile(psf) rad2, prof2 = poppy.radial_profile(psf, pa_range=[-20, 20]) rad3, prof3 = poppy.radial_profile(psf, pa_range=[-20 + 90, 20 + 90]) # Compute analytical Airy function, on exact same radial sampling as that profile. v = np.pi * rad * poppy.misc._ARCSECtoRAD * 2.0 / 1e-06 airy = ((2 * scipy.special.jn(1, v)) / v)**2 r0 = 33 # 0.33 arcsec ~ first airy ring in this case. airy_peak_envelope = airy[r0] * prof.max() / (rad / rad[r0])**3 absdiff = np.abs(prof - airy * prof.max()) if plot: import matplotlib.pyplot as plt plt.figure(figsize=(12, 6)) plt.subplot(1, 2, 1) poppy.display_psf(psf, colorbar_orientation='horizontal', title='Circular Aperture, d=2 m') plt.subplot(1, 2, 2) plt.semilogy(rad, prof) plt.semilogy(rad2, prof2, ls='--', color='red') plt.semilogy(rad3, prof3, ls=':', color='cyan') plt.semilogy(rad, airy_peak_envelope, color='gray') plt.semilogy(rad, airy_peak_envelope / 50, color='gray', alpha=0.5) plt.semilogy(rad, absdiff, color='purple') # Test the radial profile is close to the analytical Airy function. # It's hard to define relative fractional closeness for comparisons to # a function with many zero crossings; we can't just take (f1-f2)/(f1+f2) # This is a bit of a hack but let's test that the difference between # numerical and analytical is always less than 1/50th of the peaks of the # Airy function (fit based on the 1/r^3 power law fall off) assert np.all(absdiff[0:300] < airy_peak_envelope[0:300] / 50) # Test that the partial radial profiles agree with the full one. This test is # a little tricky since the sampling in r may not agree exactly. # TODO write test comparison here # Let's also test that the partial radial profiles on 90 degrees agree with each other. # These should match to machine precision. assert np.allclose(prof2, prof3) ### Next test is on a square aperture o = poppy.OpticalSystem() o.add_pupil(poppy.SquareAperture()) o.add_detector(0.010, fov_pixels=512) psf = o.calc_psf() rad, prof = poppy.radial_profile(psf) rad2, prof2 = poppy.radial_profile(psf, pa_range=[-20, 20]) rad3, prof3 = poppy.radial_profile(psf, pa_range=[-20 + 90, 20 + 90]) if plot: plt.figure(figsize=(12, 6)) plt.subplot(1, 2, 1) poppy.display_psf(psf, colorbar_orientation='horizontal', title='Square Aperture, size=1 m') plt.subplot(1, 2, 2) plt.semilogy(rad, prof) plt.semilogy(rad2, prof2, ls='--', color='red') plt.semilogy(rad3, prof3, ls=':', color='cyan') assert np.allclose(prof2, prof3)
def measure_strehl(HDUlist_or_filename=None, ext=0, slice=0, center=None, display=True, verbose=True, cache_perfect=False): """ Estimate the Strehl ratio for a PSF. This requires computing a simulated PSF with the same properties as the one under analysis. Note that this calculation will not be very accurate unless both PSFs are well sampled, preferably several times better than Nyquist. See `Roberts et al. 2004 SPIE 5490 <http://adsabs.harvard.edu/abs/2004SPIE.5490..504R>`_ for a discussion of the various possible pitfalls when calculating Strehl ratios. WARNING: This routine attempts to infer how to calculate a perfect reference PSF based on FITS header contents. It will likely work for simple direct imaging cases with WebbPSF but will not work (yet) for more complicated cases such as coronagraphy, anything with image or pupil masks, etc. Code contributions to add such cases are welcomed. Parameters ---------- HDUlist_or_filename : string Either a fits.HDUList object or a filename of a FITS file on disk ext : int Extension in that FITS file slice : int, optional If that extension is a 3D datacube, which slice (plane) of that datacube to use center : tuple center to compute around. Default is image center. If the center is on the crosshairs between four pixels, then the mean of those four pixels is used. Otherwise, if the center is in a single pixel, then that pixel is used. verbose, display : bool control whether to print the results or display plots on screen. cache_perfect : bool use caching for perfect images? greatly speeds up multiple calcs w/ same config Returns --------- strehl : float Strehl ratio as a floating point number between 0.0 - 1.0 """ from .webbpsf_core import instrument from poppy import display_psf if isinstance(HDUlist_or_filename, str): HDUlist = fits.open(HDUlist_or_filename) elif isinstance(HDUlist_or_filename, fits.HDUList): HDUlist = HDUlist_or_filename else: raise ValueError("input must be a filename or HDUlist") image = HDUlist[ext].data header = HDUlist[ext].header if image.ndim >= 3: # handle datacubes gracefully image = image[slice, :, :] if center is None: # get exact center of image # center = (image.shape[1]/2, image.shape[0]/2) center = tuple((a - 1) / 2.0 for a in image.shape[::-1]) # Compute a comparison image _log.info("Now computing image with zero OPD for comparison...") inst = instrument(header['INSTRUME']) inst.filter = header['FILTER'] inst.pupilopd = None # perfect image inst.include_si_wfe = False # perfect image inst.pixelscale = header['PIXELSCL'] * header[ 'OVERSAMP'] # same pixel scale pre-oversampling cache_key = (header['INSTRUME'], header['FILTER'], header['PIXELSCL'], header['OVERSAMP'], header['FOV'], header['NWAVES']) try: comparison_psf = _Strehl_perfect_cache[cache_key] except KeyError: comparison_psf = inst.calc_psf(fov_arcsec=header['FOV'], oversample=header['OVERSAMP'], nlambda=header['NWAVES']) if cache_perfect: _Strehl_perfect_cache[cache_key] = comparison_psf comparison_image = comparison_psf[0].data if (int(center[1]) == center[1]) and (int(center[0]) == center[0]): # individual pixel meas_peak = image[center[1], center[0]] ref_peak = comparison_image[center[1], center[0]] else: # average across a group of 4 bot = [int(np.floor(f)) for f in center] top = [int(np.ceil(f) + 1) for f in center] meas_peak = image[bot[1]:top[1], bot[0]:top[0]].mean() ref_peak = comparison_image[bot[1]:top[1], bot[0]:top[0]].mean() strehl = (meas_peak / ref_peak) if display: plt.clf() plt.subplot(121) display_psf(HDUlist, title="Observed PSF") plt.subplot(122) display_psf(comparison_psf, title="Perfect PSF") plt.gcf().suptitle("Strehl ratio = %.3f" % strehl) if verbose: print("Measured peak: {0:.3g}".format(meas_peak)) print("Reference peak: {0:.3g}".format(ref_peak)) print(" Strehl ratio: {0:.3f}".format(strehl)) return strehl
def create_image(self, coefficient_set_init=None, input_noise=None): # This is the function that creates the image (will probably call the config file) from some zernike coefficients # Copy paste the code that creates the image here import poppy from astropy.io import fits pupil_diameter = 6.559 # (in meter) As used in WebbPSF pupil_radius = pupil_diameter / 2 osys = poppy.OpticalSystem() transmission = '/Users/mygouf/Python/webbpsf/webbpsf-data4/jwst_pupil_RevW_npix1024.fits.gz' #opd = '/Users/mygouf/Python/webbpsf/webbpsf-data/NIRCam/OPD/OPD_RevV_nircam_115.fits' opd = '/Users/mygouf/Python/webbpsf/webbpsf-data4/NIRCam/OPD/OPD_RevW_ote_for_NIRCam_requirements.fits.gz' hdul = fits.open(opd) hdul2 = fits.open(transmission) # Create wavefront map #print(coefficient_set_init) zernike_coefficients = np.append(0, coefficient_set_init) #zernike_coefficients *= 1e6 # conversion from meters to microns #wavefront_map = poppy.ZernikeWFE(radius=pupil_radius, # coefficients=zernike_coefficients, # aperture_stop=False) #print(zernike_coefficients) wavefront_map = poppy.zernike.opd_from_zernikes( zernike_coefficients, npix=1024, basis=poppy.zernike.zernike_basis_faster) wavefront_map = np.nan_to_num(wavefront_map) * hdul2[0].data fits.writeto('wavefront_map.fits', wavefront_map, hdul[0].header, overwrite=True) #opd = wavefront_map opd = 'wavefront_map.fits' #myoptic = poppy.FITSOpticalElement(transmission='transfile.fits', opd='opdfile.fits', pupilscale="PIXELSCL") #opd = '/Users/mygouf/Python/webbpsf/webbpsf-data4/NIRCam/OPD/OPD_RevW_ote_for_NIRCam_requirements.fits.gz' jwst_opd = poppy.FITSOpticalElement(transmission=transmission, opd=opd) #jwst_opd = poppy.FITSOpticalElement(transmission=transmission) osys.add_pupil(jwst_opd) # JWST pupil osys.add_detector( pixelscale=0.063, fov_arcsec=self.fov_arcsec, oversample=4) # image plane coordinates in arcseconds psf = osys.calc_psf(4.44e-6) # wavelength in microns psf_poppy = np.array(psf[0].data) poppy.display_psf(psf, title='JWST NIRCam test') #psf1 = osys.calc_psf(4.44e-6) # wavelength in microns #psf2 = osys.calc_psf(2.50e-6) # wavelength in microns #psf_poppy = psf1[0].data/2 + psf2[0].data/2 psf_poppy = psf_poppy * 1e7 / np.max(psf_poppy) # Adding photon noise #image = np.random.normal(loc=psf_poppy, scale=np.sqrt(psf_poppy)) if np.all(input_noise) == None: #noise = np.random.normal(loc=psf_poppy, scale=np.sqrt(psf_poppy>1000)) #noise = self.rs.normal(loc=psf_poppy, scale=np.sqrt(psf_poppy>np.max(psf_poppy)/1000)) noise = np.random.normal( loc=psf_poppy, scale=np.sqrt(psf_poppy > np.max(psf_poppy) / 1000)) #noise = rs.poisson(psf_poppy) #print('Estimated Noise', np.mean(noise)) else: noise = input_noise #print('Input Noise', np.mean(noise)) image = psf_poppy + noise dict_ = { 'image': image, 'noise': noise, 'wavefront_map': wavefront_map } #print(np.mean(image),np.mean(noise)) return dict_
def create_image_from_opd_file(self, opd=None, input_noise=None): # This is the function that creates the image (will probably call the config file) from some zernike coefficients # Copy paste the code that creates the image here import poppy from astropy.io import fits pupil_diameter = 6.559 # (in meter) As used in WebbPSF pupil_radius = pupil_diameter / 2 osys = poppy.OpticalSystem() transmission = '/Users/mygouf/Python/webbpsf/webbpsf-data4/jwst_pupil_RevW_npix1024.fits.gz' #opd = '/Users/mygouf/Python/webbpsf/webbpsf-data/NIRCam/OPD/OPD_RevV_nircam_115.fits' #opd = '/Users/mygouf/Python/webbpsf/webbpsf-data4/NIRCam/OPD/OPD_RevW_ote_for_NIRCam_requirements.fits.gz' hdul = fits.open(opd) hdul2 = fits.open(transmission) wavefront_map = hdul[0].data jwst_opd = poppy.FITSOpticalElement(transmission=transmission, opd=opd) #jwst_opd = poppy.FITSOpticalElement(transmission=transmission) osys.add_pupil(jwst_opd) # JWST pupil osys.add_detector( pixelscale=0.063, fov_arcsec=self.fov_arcsec, oversample=4) # image plane coordinates in arcseconds psf = osys.calc_psf(4.44e-6) # wavelength in microns psf_poppy = np.array(psf[0].data) poppy.display_psf(psf, title='JWST NIRCam test') #psf1 = osys.calc_psf(4.44e-6) # wavelength in microns #psf2 = osys.calc_psf(2.50e-6) # wavelength in microns #psf_poppy = psf1[0].data/2 + psf2[0].data/2 psf_poppy = psf_poppy * 1e7 / np.max(psf_poppy) # Adding photon noise #image = np.random.normal(loc=psf_poppy, scale=np.sqrt(psf_poppy)) if np.all(input_noise) == None: #noise = np.random.normal(loc=psf_poppy, scale=np.sqrt(psf_poppy>1000)) #noise = self.rs.normal(loc=psf_poppy, scale=np.sqrt(psf_poppy>np.max(psf_poppy)/1000)) noise = np.random.normal( loc=psf_poppy, scale=np.sqrt(psf_poppy > np.max(psf_poppy) / 1000)) #noise = rs.poisson(psf_poppy) #print('Estimated Noise', np.mean(noise)) else: noise = input_noise #print('Input Noise', np.mean(noise)) image = psf_poppy + noise dict_ = { 'image': image, 'noise': noise, 'wavefront_map': wavefront_map } #print(np.mean(image),np.mean(noise)) return dict_
def test_radial_profile(plot=False): """ Test radial profile calculation, including circular and square apertures, and including with the pa_range option. """ ### Tests on a circular aperture o = poppy_core.OpticalSystem() o.add_pupil(poppy.CircularAperture(radius=1.0)) o.add_detector(0.010, fov_pixels=512) psf = o.calc_psf() rad, prof = poppy.radial_profile(psf) rad2, prof2 = poppy.radial_profile(psf, pa_range=[-20,20]) rad3, prof3 = poppy.radial_profile(psf, pa_range=[-20+90, 20+90]) # Compute analytical Airy function, on exact same radial sampling as that profile. v = np.pi* rad*poppy.misc._ARCSECtoRAD * 2.0/1e-06 airy = ((2*scipy.special.jn(1, v))/v)**2 r0 = 33 # 0.33 arcsec ~ first airy ring in this case. airy_peak_envelope = airy[r0]*prof.max() / (rad/rad[r0])**3 absdiff = np.abs(prof - airy*prof.max()) if plot: import matplotlib.pyplot as plt plt.figure(figsize=(12,6)) plt.subplot(1,2,1) poppy.display_psf(psf, colorbar_orientation='horizontal', title='Circular Aperture, d=2 m') plt.subplot(1,2,2) plt.semilogy(rad,prof) plt.semilogy(rad2,prof2, ls='--', color='red') plt.semilogy(rad3,prof3, ls=':', color='cyan') plt.semilogy(rad, airy_peak_envelope, color='gray') plt.semilogy(rad, airy_peak_envelope/50, color='gray', alpha=0.5) plt.semilogy(rad, absdiff, color='purple') # Test the radial profile is close to the analytical Airy function. # It's hard to define relative fractional closeness for comparisons to # a function with many zero crossings; we can't just take (f1-f2)/(f1+f2) # This is a bit of a hack but let's test that the difference between # numerical and analytical is always less than 1/50th of the peaks of the # Airy function (fit based on the 1/r^3 power law fall off) assert np.all( absdiff[0:300] < airy_peak_envelope[0:300]/50) # Test that the partial radial profiles agree with the full one. This test is # a little tricky since the sampling in r may not agree exactly. # TODO write test comparison here # Let's also test that the partial radial profiles on 90 degrees agree with each other. # These should match to machine precision. assert np.allclose(prof2, prof3) ### Next test is on a square aperture o = poppy.OpticalSystem() o.add_pupil(poppy.SquareAperture()) o.add_detector(0.010, fov_pixels=512) psf = o.calc_psf() rad, prof = poppy.radial_profile(psf) rad2, prof2 = poppy.radial_profile(psf, pa_range=[-20,20]) rad3, prof3 = poppy.radial_profile(psf, pa_range=[-20+90, 20+90]) if plot: plt.figure(figsize=(12,6)) plt.subplot(1,2,1) poppy.display_psf(psf, colorbar_orientation='horizontal', title='Square Aperture, size=1 m') plt.subplot(1,2,2) plt.semilogy(rad,prof) plt.semilogy(rad2,prof2, ls='--', color='red') plt.semilogy(rad3,prof3, ls=':', color='cyan') assert np.allclose(prof2, prof3)
logging.basicConfig(level=logging.DEBUG) np.seterr(divide='ignore', invalid='ignore') osys = poppy.OpticalSystem() osys.add_pupil(poppy.CircularAperture(radius=3)) # pupil radius in meters osys.add_detector(pixelscale=0.010, fov_arcsec=5.0) 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) 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)
def test_ThinLens(display=False): """ Test that a +0.5 wave lens creates +0.5 waves of OPD """ pupil_radius = 1 # let's add < 1 wave here so we don't have to worry about wrapping lens = optics.ThinLens(nwaves=0.5, reference_wavelength=1e-6, radius=pupil_radius) # n.b. npix is 99 so that there are an integer number of pixels per meter (hence multiple of 3) # and there is a central pixel at 0,0 (hence odd npix) # Otherwise the strict test against half a wave min max doesn't work # because we're missing some (tiny but nonzero) part of the aperture wave = poppy_core.Wavefront(npix=99, diam=3.0, wavelength=1e-6) wave *= lens # Now test the values at some precisely chosen pixels y, x = wave.coordinates() at_radius = ((x == 1) & (y == 0)) assert np.allclose(wave.phase[at_radius], np.pi / 2), "Didn't get 1/2 wave OPD at edge of optic" assert len( at_radius[0] ) > 0, "Array indices messed up - need to have a pixel at exactly (1,0)" at_radius = ((x == 0) & (y == 1)) assert np.allclose(wave.phase[at_radius], np.pi / 2), "Didn't get 1/2 wave OPD at edge of optic" assert len( at_radius[0] ) > 0, "Array indices messed up - need to have a pixel at exactly (0,1)" at_center = ((x == 0) & (y == 0)) assert np.allclose(wave.phase[at_center], -np.pi / 2), "Didn't get -1/2 wave OPD at center of optic" assert len( at_radius[0] ) > 0, "Array indices messed up - need to have a pixel at exactly (0,0)" # TODO test intermediate pixel values between center and edge? # OK - This is now tested in test_sign_conventions.test_lens_wfe_sign # regression test to ensure null optical elements don't change ThinLens behavior # see https://github.com/mperrin/poppy/issues/14 osys = poppy_core.OpticalSystem() osys.add_pupil(optics.CircularAperture(radius=1)) for i in range(3): osys.add_image() osys.add_pupil() osys.add_pupil( optics.ThinLens(nwaves=0.5, reference_wavelength=1e-6, radius=pupil_radius)) osys.add_detector(pixelscale=0.01, fov_arcsec=3.0) psf = osys.calc_psf(wavelength=1e-6) osys2 = poppy_core.OpticalSystem() osys2.add_pupil(optics.CircularAperture(radius=1)) osys2.add_pupil( optics.ThinLens(nwaves=0.5, reference_wavelength=1e-6, radius=pupil_radius)) osys2.add_detector(pixelscale=0.01, fov_arcsec=3.0) psf2 = osys2.calc_psf() if display: import poppy poppy.display_psf(psf) poppy.display_psf(psf2) assert np.allclose(psf[0].data, psf2[0].data), ( "ThinLens shouldn't be affected by null optical elements! Introducing extra image planes " "made the output PSFs differ beyond numerical tolerances.")
import poppy import numpy as np import matplotlib.pyplot as plt import logging logging.basicConfig(level=logging.DEBUG) np.seterr(divide='ignore', invalid='ignore') 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)