def calc(*args): progress.visible = True if monochromatic_toggle.value is True: psf = instrument.calc_psf( monochromatic=monochromatic_wavelength.value * 1e-6, display=True, outfile=OUTPUT_FILENAME, overwrite=True ) else: source = poppy.specFromSpectralType(source_selection.value) _log.debug("Got source type {}: {}".format(source_selection.value, source)) psf = instrument.calc_psf( source=source, display=True, outfile=OUTPUT_FILENAME, overwrite=True ) fig, (ax_oversamp, ax_detsamp) = plt.subplots(1, 2,figsize=(12, 4)) title1 = "PSF sim for {}, {}\n".format(instrument.name, instrument.filter) poppy.display_PSF(psf, ax=ax_oversamp, title=title1+"Oversampled PSF") poppy.display_PSF(psf, ax=ax_detsamp, ext='DET_SAMP', title=title1+'Detector pixel sampled PSF') progress.visible = None download_link.visible = True
def calc(*args): progress.visible = True if monochromatic_toggle.value is True: psf = instrument.calcPSF( monochromatic=monochromatic_wavelength.value * 1e-6, display=True, outfile=OUTPUT_FILENAME, overwrite=True) else: source = poppy.specFromSpectralType(source_selection.value) _log.debug("Got source type {}: {}".format(source_selection.value, source)) psf = instrument.calcPSF(source=source, display=True, outfile=OUTPUT_FILENAME, overwrite=True) fig, (ax_oversamp, ax_detsamp) = plt.subplots(1, 2, figsize=(12, 4)) title1 = "PSF sim for {}, {}\n".format(instrument.name, instrument.filter) poppy.display_PSF(psf, ax=ax_oversamp, title=title1 + "Oversampled PSF") poppy.display_PSF(psf, ax=ax_detsamp, ext='DET_SAMP', title=title1 + 'Detector pixel sampled PSF') progress.visible = None download_link.visible = True
def validate_miri_coron(): """Validate MIRI coronagraphic performance against the simulations in Cavarroc et al. 2008 SPIE """ miri = webbpsf_core.MIRI() miri.filter = 'FND' miri.image_mask = 'FQPM1140' im_nd_onaxis = miri.calcPSF(fov_arcsec=10) miri.options['source_offset_x'] = 2 miri.options['source_offset_y'] = 2 im_nd_offaxis = miri.calcPSF(fov_arcsec=10) P.subplot(211) poppy.display_PSF(im_nd_offaxis, colorbar=False) P.subplot(212) poppyt.display_PSF(im_nd_onaxis, colorbar=False) stop()
def ev_displayPSF(self): "Event handler for Displaying the PSF" #self._updateFromGUI() #if self.PSF_HDUlist is not None: plt.clf() poppy.display_PSF(self.PSF_HDUlist, vmin = self.advanced_options['psf_vmin'], vmax = self.advanced_options['psf_vmax'], scale = self.advanced_options['psf_scale'], cmap= self.advanced_options['psf_cmap'], normalize=self.advanced_options['psf_normalize']) self._refresh_window()
def test_MatrixFT_FFT_Lyot_propagation_equivalence(display=False): """ Using a simple Lyot coronagraph prescription, perform a simple numerical check for consistency between calcPSF 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.addPupil(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.addImage(annFPM) fftcoron_annFPM_osys.addPupil(poppy.CircularAperture(radius=0.9 * D / 2)) fftcoron_annFPM_osys.addDetector(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.calcPSF(wavelen) annFPM_mft_psf = matrixFTcoron_annFPM_osys.calcPSF(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_ThinLens(display=False): pupil_radius = 1 pupil = optics.CircularAperture(radius=pupil_radius) # 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 *= pupil wave *= lens assert np.allclose(wave.phase.max(), np.pi / 2) assert np.allclose(wave.phase.min(), -np.pi / 2) # 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.addPupil(optics.CircularAperture(radius=1)) for i in range(3): osys.addImage() osys.addPupil() osys.addPupil( optics.ThinLens(nwaves=0.5, reference_wavelength=1e-6, radius=pupil_radius)) osys.addDetector(pixelscale=0.01, fov_arcsec=3.0) psf = osys.calcPSF(wavelength=1e-6) osys2 = poppy_core.OpticalSystem() osys2.addPupil(optics.CircularAperture(radius=1)) osys2.addPupil( optics.ThinLens(nwaves=0.5, reference_wavelength=1e-6, radius=pupil_radius)) osys2.addDetector(pixelscale=0.01, fov_arcsec=3.0) psf2 = osys2.calcPSF() 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.")
def test_MFT_FFT_equivalence_in_OpticalSystem(display=False): """ 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.""" # 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. fits511 = optics.ParityTestAperture().to_fits('test.fits', 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() 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)) 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 test_MFT_FFT_equivalence_in_OpticalSystem(display=False): """ 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.""" # 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. fits511 = optics.ParityTestAperture().to_fits('test.fits', 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() 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)) 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 make_hawki_poppy_psf(): import poppy osys = poppy.OpticalSystem() m1 = poppy.CircularAperture(radius=4.1) spiders = poppy.SecondaryObscuration(secondary_radius=0.6, n_supports=4, support_width=0.1) vlt = poppy.CompoundAnalyticOptic(opticslist=[m1, spiders], name='VLT') psfs = [] seeing = 0.4 see_psf = simcado.psf.seeing_psf(fwhm=seeing, size=384, pix_res=0.106 / 2) for lam in [1.2, 1.6, 2.2]: osys = poppy.OpticalSystem() osys.add_pupil(vlt) osys.add_detector(pixelscale=0.106, fov_arcsec=0.106 * 192) diff_psf = osys.calc_psf(lam * 1e-6) tmp = deepcopy(diff_psf) tmp[0].data = fftconvolve(diff_psf[0].data, see_psf[0].data, mode="same") tmp[0].data /= np.sum(tmp[0].data) tmp[0].header["SEEING"] = seeing tmp[0].header["CDELT1"] = tmp[0].header["PIXELSCL"] tmp[0].header["CDELT2"] = tmp[0].header["PIXELSCL"] psfs += tmp hdus = fits.HDUList(psfs) for i in range(1, len(hdus)): hdus[i] = fits.ImageHDU(data=hdus[i].data, header=hdus[i].header) hdus.writeto("HAWK-I_config/PSF_HAWKI_poppy.fits", clobber=True) fname = "HAWK-I_config/PSF_HAWKI_poppy.fits" plt.figure(figsize=(15, 4)) for i in range(3): plt.subplot(1, 3, i + 1) poppy.display_PSF(fname, ext=i, title="HAWKI PSF lambda=" + str(fits.getheader(fname, ext=i)["WAVELEN"])) plt.show()
def test_ThinLens(display=False): pupil_radius = 1 pupil = optics.CircularAperture(radius=pupil_radius) # 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 *= pupil wave *= lens assert np.allclose(wave.phase.max(), np.pi/2) assert np.allclose(wave.phase.min(), -np.pi/2) # 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.addPupil(optics.CircularAperture(radius=1)) for i in range(3): osys.addImage() osys.addPupil() osys.addPupil(optics.ThinLens(nwaves=0.5, reference_wavelength=1e-6, radius=pupil_radius)) osys.addDetector(pixelscale=0.01, fov_arcsec=3.0) psf = osys.calcPSF(wavelength=1e-6) osys2 = poppy_core.OpticalSystem() osys2.addPupil(optics.CircularAperture(radius=1)) osys2.addPupil(optics.ThinLens(nwaves=0.5, reference_wavelength=1e-6, radius=pupil_radius)) osys2.addDetector(pixelscale=0.01, fov_arcsec=3.0) psf2 = osys2.calcPSF() 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." )
def test_MatrixFT_FFT_Lyot_propagation_equivalence(display=False): """ Using a simple Lyot coronagraph prescription, perform a simple numerical check for consistency between calcPSF 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.addPupil( 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.addImage( annFPM ) fftcoron_annFPM_osys.addPupil( poppy.CircularAperture(radius=0.9*D/2) ) fftcoron_annFPM_osys.addDetector( 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.calcPSF(wavelen) annFPM_mft_psf = matrixFTcoron_annFPM_osys.calcPSF(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 calc(*args): progress.visible = True if monochromatic_toggle.value is True: psf = instrument.calcPSF( monochromatic=monochromatic_wavelength.value * 1e-6, display=True, outfile=OUTPUT_FILENAME, overwrite=True) else: source = poppy.specFromSpectralType(source_selection.value) _log.debug("Got source type {}: {}".format(source_selection.value, source)) psf = instrument.calcPSF(source=source, display=True, outfile=OUTPUT_FILENAME, overwrite=True) fig, (ax_oversamp, ax_detsamp) = plt.subplots(1, 2) poppy.display_PSF(psf, ax=ax_oversamp) poppy.display_PSF(psf, ax=ax_detsamp, ext='DET_SAMP') progress.visible = None download_link.visible = True
def calc(*args): progress.visible = True if monochromatic_toggle.value is True: psf = instrument.calc_psf( monochromatic=monochromatic_wavelength.value * 1e-6, display=True, outfile=OUTPUT_FILENAME, overwrite=True ) else: source = poppy.specFromSpectralType(source_selection.value) _log.debug("Got source type {}: {}".format(source_selection.value, source)) psf = instrument.calc_psf( source=source, display=True, outfile=OUTPUT_FILENAME, overwrite=True ) fig, (ax_oversamp, ax_detsamp) = plt.subplots(1, 2) poppy.display_PSF(psf, ax=ax_oversamp) poppy.display_PSF(psf, ax=ax_detsamp, ext='DET_SAMP') progress.visible = None download_link.visible = True
def do_test_source_offset(self, iname, theta=0.0): nc = webbpsf.Instrument(iname) nc.pupilopd=None nsteps = 3 oversample = 2 shift_req = [] psfs = [] for i in range(nsteps+1): nc.options['source_offset_r'] = i*0.1 nc.options['source_offset_theta'] = theta nc.options['source_offset_r'] = i*nc.pixelscale*5 shift_req.append(nc.options['source_offset_r']) psfs.append( nc.calcPSF(nlambda=1, oversample=oversample) ) poppy.display_PSF(psfs[0]) cent0 = np.asarray(poppy.measure_centroid(psfs[0])) center_pix = (psfs[0][0].data.shape[0]-1)/2.0 self.assertAlmostEqual(cent0[0], center_pix, 3) self.assertAlmostEqual(cent0[1], center_pix, 3) _log.info("Center of unshifted image: (%d, %d)" % tuple(cent0)) for i in range(1, nsteps+1): poppy.display_PSF(psfs[i]) cent = poppy.measure_centroid(psfs[i]) rx = shift_req[i] * (-np.sin(theta*np.pi/180)) ry = shift_req[i] * (np.cos(theta*np.pi/180)) _log.info(" Shift_requested:\t(%10.3f, %10.3f)" % (rx, ry)) shift = (cent-cent0) * (nc.pixelscale/oversample) _log.info(" Shift_achieved: \t(%10.3f, %10.3f)" % (shift[1], shift[0])) self.assertAlmostEqual(rx, shift[1], 3) self.assertAlmostEqual(ry, shift[0], 3)
def do_test_source_offset(iname, distance=0.5, nsteps=1, theta=0.0, tolerance=0.05, monochromatic=None, display=False): """ Test source offsets Does the star PSF center end up in the desired location? The tolerance threshold for success is by default 1/20th of a pixel in the SI pixel units. But this can be adjusted by the calling function if needed. This is chosen somewhat arbitrarily as pretty good subpixel performance for most applications. Trying for greater accuracy would be limited by subpixel sampling in the simulations, as well as by the accuracy of the centroid measuring function itself. """ _log.info("Calculating shifted image PSFs for "+iname) si = webbpsf_core.Instrument(iname) si.pupilopd=None if iname=='NIRSpec': si.image_mask = None # remove default MSA since it overcomplicates this test. oversample = 2 # Calculations shift_req = [] psfs = [] # unshifted PSF #psfs.append( nc.calc_psf(nlambda=1, oversample=oversample) ) #shift_req.append(0) steps = np.linspace(0, distance, nsteps+1) for i, value in enumerate(steps): si.options['source_offset_r'] = steps[i] si.options['source_offset_theta'] = theta #nc.options['source_offset_r'] = i*nc.pixelscale*5 shift_req.append(si.options['source_offset_r']) psfs.append( si.calc_psf(nlambda=1, monochromatic=monochromatic, oversample=oversample) ) # Control case: an unshifted image cent0 = np.asarray(poppy.measure_centroid(psfs[0])) center_pix = (psfs[0][0].data.shape[0]-1)/2.0 assert( abs(cent0[0] == center_pix) < 1e-3 ) assert( abs(cent0[1] == center_pix) < 1e-3 ) _log.info("Center of unshifted image: ({0:.3f}, {1:.3f}) pixels measured".format(*cent0)) _log.info(" vs center of the array is ({0}, {0})".format(center_pix)) if display: poppy.display_PSF(psfs[0]) # Compare to shifted case(s) for i in range(1, nsteps+1): if display: poppy.display_PSF(psfs[i]) cent = poppy.measure_centroid(psfs[i]) rx = shift_req[i] * (-np.sin(theta*np.pi/180)) ry = shift_req[i] * (np.cos(theta*np.pi/180)) _log.info(" Shift_requested:\t(%10.3f, %10.3f) arcsec" % (rx, ry)) shift = (cent-cent0) * (si.pixelscale/oversample) _log.info(" Shift_achieved: \t(%10.3f, %10.3f) arcsec" % (shift[1], shift[0])) deltax = abs(rx - shift[1]) deltay = abs(ry - shift[0]) _log.info(" X offset:\t{0:.3f}\t\tTolerance:\t{1:.3f}".format(deltax, (si.pixelscale*tolerance))) assert( deltax < (si.pixelscale*tolerance) ) _log.info(" Y offset:\t{0:.3f}\t\tTolerance:\t{1:.3f}".format(deltay, (si.pixelscale*tolerance))) assert( deltay < (si.pixelscale*tolerance) )
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, six.string_types): 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.calcPSF(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 = [np.floor(f) for f in center] top = [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 test_ThinLens(display=False): 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 = np.where((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 = np.where((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 = np.where((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? # 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.")
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, six.string_types): 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.calcPSF(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 = [np.floor(f) for f in center] top = [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 validate_vs_krist_sims(clobber=False, normalize=False, which='spot', no_sam=False): """ Compare with PSFs provided by John Krist """ if which=='spot': image_mask = 'MASK430R' pupil_mask = 'CIRCLYOT' else: image_mask = 'MASKLWB' pupil_mask = 'WEDGELYOT' P.subplots_adjust(left=0.07, right=0.95, top=0.9, bottom=0.05) nc = webbpsf_core.NIRCam() nc.pupilopd=None nc.filter = 'F460M' nc.image_mask = image_mask nc.pupil_mask = pupil_mask nc.options['no_sam'] = no_sam nc.pixelscale = 0.065 # match the Krist sims exacly. vs 0.648 official cor_vmin = 1e-12 cor_vmax=1e-5 P.clf() fig = P.gcf() fig.text(0.2, 0.95, 'Krist', horizontalalignment='center', size=18) fig.text(0.50, 0.95, 'Perrin', horizontalalignment='center', size=18) fig.text(0.80, 0.95, 'Difference P-K', horizontalalignment='center', size=18) fig.text(0.05, 1./6, 'off-axis 4.6$\mu$m', verticalalignment='center', rotation='vertical' , size=18) fig.text(0.05, 0.48, 'occulted 4.6$\mu$m', verticalalignment='center', rotation='vertical' , size=18) fig.text(0.05, 5./6-0.05, image_mask + ' occulter', verticalalignment='center', rotation='vertical' , size=18) P.subplot(331) mask1 = 'nircam_4600nm_%s_occ.fits' % which mask1f = fits.open(mask1) poppy.display_PSF(mask1f, title="", pixelscale='PIXSIZE', vmin=0, vmax=1, scale='linear', cmap=matplotlib.cm.gray) P.subplot(332) os = nc._get_optical_system() os.planes[1].display(ax=P.gca(), what='intensity', colorbar_orientation='vertical') P.gca().set_title('') P.gca().set_xbound(-8,8) P.gca().set_ybound(-8,8) wf = poppy.Wavefront(wavelength=4.6e-6, npix = mask1f[0].data.shape[0], pixelscale = mask1f[0].header['PIXSIZE']) trans = os.planes[1].getPhasor(wf)**2 P.subplot(333) if normalize: to_plot = (trans-mask1f[0].data) / (trans+mask1f[0].data)/2 vmin, vmax = -1, 1 else: to_plot = (trans-mask1f[0].data) vmin, vmax = -1e-3, 1e-3 poppy.imshow_with_mouseover(to_plot, cmap=matplotlib.cm.gray, vmin=vmin, vmax=vmax, extent=[-8, 8, -8, 8]) P.colorbar(P.gca().images[0], orientation='vertical') try: fits.PrimaryHDU(trans).writeto('test_nircam_4600nm_%s_occ.fits' % which, clobber=clobber) except: pass #---- occulted -- P.subplot(334) k1 = 'nircam_4600nm_%s.fits' % which k1f = fits.open(k1) print("Total of %s is %f" % (k1, k1f[0].data.sum())) poppy.display_PSF(k1f, title="", pixelscale='SAMPLING', vmin=cor_vmin, vmax=cor_vmax) P.subplot(335) my1 = 'test_'+k1 mypsf1 = webbpsf_core.calc_or_load_psf('test_'+k1, nc, nlambda=1,monochromatic=4.6e-6, oversample=4, fov_pixels=247, clobber=clobber) print("Total of %s is %f" % (my1, mypsf1[0].data.sum())) #nc.calcPSF(nlambda=1) poppy.display_PSF(mypsf1, ext=1, title="", adjust_for_oversampling=True, vmin=cor_vmin, vmax=cor_vmax) P.subplot(336) poppy.display_PSF_difference( mypsf1, k1f, ext2=0, ext1=1, title="", vmax=1e-7, normalize=normalize) #---- unocculted -- P.subplot(337) k2 = 'nircam_4600nm_%s_fieldpsf.fits' % which k2f = fits.open(k2) poppy.display_PSF(k2f, title="", pixelscale='SAMPLING') print("Total of %s is %f" % (k2, k2f[0].data.sum())) nc.image_mask = None # make a coronagraphic-off-axis type PSF but still on-axis in the array my2 = 'test_'+k2 mypsf2 = webbpsf_core.calc_or_load_psf('test_'+k2, nc, nlambda=1, monochromatic=4.6e-6, oversample=4, fov_pixels=247, clobber=clobber) P.subplot(338) poppy.display_PSF(mypsf2, title="", ext=1, adjust_for_oversampling=True) print("Total of %s is %f" % (my2, mypsf2[0].data.sum())) P.subplot(339) poppy.display_PSF_difference( mypsf2, k2f, ext2=0, ext1=1, title="", vmax=1e-5, normalize=normalize) print("shape of %s is %s" % (k1, k1f[0].data.shape)) print("shape of %s is %s" % (my1, mypsf1[1].data.shape)) P.savefig('results_nircam_coron_comparison_%s.pdf' % which) stop()
def validate_vs_jwpsf_nircam(): """ Compare results from WebbPSF with earlier simulations produced with JWPSF """ models = [ ('NIRCam','F200W', 'f200w_perfect_offset', '/Users/mperrin/software/jwpsf_v3.0/data/NIRCam/OPD/perfect_opd.fits', 0.034,True), ('NIRCam','F200W', 'f200w_perfect', '/Users/mperrin/software/jwpsf_v3.0/data/NIRCam/OPD/perfect_opd.fits', 0.034,False), ('NIRCam','F200W', 'f200w', '/Users/mperrin/software/jwpsf_v3.0/data/NIRCam/OPD/nircam_obs_w_rsrv1.fits', 0.034,True), ('MIRI','F1000W', 'f1000w', '/Users/mperrin/software/jwpsf_v3.0/data/MIRI/OPD/MIRI_OPDisim1.fits', 0.11,True)] fig = P.figure(1, figsize=(13,8.5), dpi=80) oversamp=4 for params in models: nc = webbpsf_core.Instrument(params[0]) nc.filter = params[1] nc.pupilopd = params[3] #'/Users/mperrin/software/jwpsf_v3.0/data/NIRCam/OPD/nircam_obs_w_rsrv1.fits' nc.pixelscale = params[4] #0.034 # this is wrong, but compute this way to match JWPSF exactly if params[5]: # offset by half a pixel to match the JWPSF convention nc.options['source_offset_r'] = params[4]/2 * N.sqrt(2)/oversamp # offset half a pixel each in X and Y nc.options['source_offset_theta'] = -45 jw_fn = 'jwpsf_%s_%s.fits' % (params[0].lower(), params[2].lower()) my_fn = 'test_vs_' + jw_fn if not os.path.exists( my_fn): my_psf = nc.calcPSF(my_fn, oversample=oversamp, fov_pixels=512./oversamp) else: my_psf = fits.open(my_fn) jw_psf = fits.open(jw_fn) jw_psf[0].header.update('PIXELSCL', jw_psf[0].header['CDELT1']*3600) P.clf() #P.subplots_adjust(top=0.95, bottom=0.05, left=0.01, right=0.99) P.subplot(231) titlestr = "%s %s, \n"% (params[0], params[2]) poppy.display_PSF(my_psf, title=titlestr+"computed with WebbPSF" , colorbar=False) P.subplot(232) poppy.display_PSF(jw_psf, title=titlestr+"computed with JWPSF" , colorbar=False) P.subplot(233) poppy.display_PSF_difference(my_psf,jw_psf, title=titlestr+'Difference Image', colorbar=False) imagecrop = 30*params[4] P.subplot(234) poppy.display_PSF(my_psf, title=titlestr+"computed with WebbPSF", colorbar=False, imagecrop=imagecrop) centroid = poppy.measure_centroid(my_psf) P.gca().set_xlabel("centroid = (%.3f,%.3f)" % centroid) P.subplot(235) poppy.display_PSF(jw_psf, title=titlestr+"computed with JWPSF", colorbar=False, imagecrop=imagecrop) centroid = poppy.measure_centroid(jw_psf) P.gca().set_xlabel("centroid = (%.3f,%.3f)" % centroid) P.subplot(236) poppy.display_PSF_difference(my_psf,jw_psf, title='Difference Image', colorbar=False, imagecrop=imagecrop) P.savefig("results_vs_jwpsf_%s_%s.pdf" % (params[0], params[2]))
def make_fig_instrument_comparison(): """ Make the instrument comparison figure for the webbPSF web page. Argh I already wrote this once, now I need to write it again 'cause I can't find it... """ # make figure size = array([ 14.325, 2.975]) in inches plt.clf() f, axarr = plt.subplots(1, 6, sharex=True, num=1, sharey=True) nc = webbpsf.NIRCam() nc.filter='F210M' webbpsf.calc_or_load_PSF('NIRCam_demo_F212.fits', nc, oversample=4, rebin=True, fov_arcsec=5) poppy.display_PSF('NIRCam_demo_F212.fits', ext=1, ax= axarr[0], colorbar=False, title="NIRCam F210M") nc = webbpsf.NIRCam() nc.filter='F444W' webbpsf.calc_or_load_PSF('NIRCam_demo_F444.fits', nc, oversample=4, rebin=True, fov_arcsec=5) poppy.display_PSF('NIRCam_demo_F444.fits', ext=1, ax= axarr[1], colorbar=False, title="NIRCam F444W") ns = webbpsf.NIRSpec() ns.filter='F110W' webbpsf.calc_or_load_PSF('NIRSpec_demo.fits', ns, oversample=4, rebin=True, fov_arcsec=5) poppy.display_PSF('NIRSpec_demo.fits', ext=1, ax= axarr[2], colorbar=False, title="NIRspec F110W") ni = webbpsf.NIRISS() ni.filter='F380M' webbpsf.calc_or_load_PSF('NIRISS_demo.fits', ni, oversample=4, rebin=True, fov_arcsec=5) poppy.display_PSF('NIRISS_demo.fits', ext=1, ax= axarr[3], colorbar=False, title="NIRISS F380M") mi = webbpsf.MIRI() mi.filter='F1000W' webbpsf.calc_or_load_PSF('MIRI_demo.fits', mi, oversample=4, rebin=True, fov_arcsec=5) poppy.display_PSF('MIRI_demo.fits', ext=1, ax= axarr[4], colorbar=False, title="MIRI F1000W") fg = webbpsf.FGS() webbpsf.calc_or_load_PSF('FGS_demo.fits', fg, oversample=4, rebin=True, fov_arcsec=5) poppy.display_PSF('FGS_demo.fits', ext=1, ax= axarr[5], colorbar=False, title="FGS") plt.subplots_adjust(hspace=0.01) for ax in axarr: ax.set_ylim(-2.5, 2.5) ax.set_xlim(-2.5, 2.5) plt.savefig('fig_instrument_comparison.pdf')
def compare_pupils_tv( oversample=8, vmax=1e-5, skipone=True): """ Compare PSFs with the Rev T and Rev V pupil shapes """ P.clf() inst = webbpsf_core.NIRCam() inst.pupilopd=None fov_arcsec = 10 nlambda=30 inst.pupil = 'tricontagon.fits' psf_tri = webbpsf_core.calc_or_load_psf('test_NIRCam_perfect_tricontagon_o%d.fits' % oversample, inst, nlambda=nlambda, oversample=oversample, fov_arcsec=fov_arcsec) if not skipone: ax = P.subplot(1,3,1) poppy.display_PSF(psf_tri, normalize='peak', colorbar=False, title='Tricontagon') for i, rev in enumerate(['T','V']): inst.pupil = "pupil_Rev%s.fits" % rev psf = webbpsf_core.calc_or_load_psf('test_NIRCam_perfect_rev%s_o%d.fits' % (rev, oversample),inst, nlambda=nlambda, oversample=oversample, fov_arcsec=fov_arcsec) P.subplot(1,3,i+2) poppy.display_PSF(psf, normalize='peak', colorbar = (rev =='V'), title='OTE Rev '+rev) stop() P.clf() psf_V = fits.open('test_NIRCam_perfect_rev%s_o%d.fits' % ('V', oversample)) psf_T = fits.open('test_NIRCam_perfect_rev%s_o%d.fits' % ('T', oversample)) P.subplot(221) poppy.display_PSF_difference(psf_V, psf_tri, vmax=vmax, title="Rev V - tricontagon") P.subplot(222) poppy.display_PSF_difference(psf_V, psf_T,vmax=vmax, title="Rev V - Rev T") ax3 = P.subplot(223) ax3.set_ylabel('Azimuthally averaged profile') ax3.set_xlabel('Separation (arcsec)') ax4 = P.subplot(224) ax4.set_ylabel('Fractional Encircled Energy') ax4.set_xlabel('Separation (arcsec)') for p, label in zip([psf_tri, psf_T, psf_V], ['Tri', "Rev T", 'Rev V']): rad, rp = poppy.radial_profile(p) ee_fn = poppy.measure_EE(p) ax3.plot(rad,rp/rp.max()) ax4.plot(rad, ee_fn(rad), label=label) print(poppy.measure_fwhm(p)) ax4.legend(loc='lower right') ax3.set_yscale('log') ax3.set_xscale('log') #ax3.axhline([psf_V[0].data.max()*0.5], ls=":") #ax3.set_xbound(0,4) ax3.set_xbound(0.01,4) ax4.set_xbound(0,4) P.draw() stop()