def test_TipTiltStage(display=False, verbose=False): """ Test tip tilt stage moves the PSF by the requested amount """ ap = poppy.HexagonAperture(flattoflat=0.75 * u.m) det = poppy.Detector(pixelscale=0.1 * u.arcsec / u.pix, fov_pixels=128) tt = poppy.active_optics.TipTiltStage(ap, include_factor_of_two=False) wave = poppy.Wavefront(npix=128, diam=1 * u.m) trans = ap.get_transmission(wave) assert np.allclose(tt.get_transmission(wave), trans), "Transmission does not match expectations" assert np.allclose(tt.get_opd(wave), 0), "OPD without tilt does not match expectation" for tx, ty in ((0 * u.arcsec, 1 * u.arcsec), (1 * u.arcsec, 0 * u.arcsec), (-0.23 * u.arcsec, 0.65 * u.arcsec)): for include_factor_of_two in [True, False]: if verbose: print( f"Testing {tx}, {ty}, with include_factor_of_two={include_factor_of_two}" ) tt.include_factor_of_two = include_factor_of_two tt.set_tip_tilt(tx, ty) wave = poppy.Wavefront(npix=64, diam=1 * u.m) wave *= ap wave *= tt if display: plt.figure() wave.display(what='both') plt.suptitle(f"Wavefront with {tx}, {ty}") wave.propagate_to(det) if display: plt.figure() wave.display() plt.title(f"PSF with {tx}, {ty}") cen = poppy.measure_centroid(wave.as_fits(), boxsize=5, relativeto='center', units='arcsec') factor = 2 if include_factor_of_two else 1 assert np.isclose(cen[1] * u.arcsec, tx * factor, atol=1e-4), "X pos not as expected" assert np.isclose( cen[0] * u.arcsec, ty * factor, atol=1e-4 ), f"Y pos not as expected: {cen[0]*u.arcsec}, {ty*factor}"
def test_wavefront_tilt_sign_and_direction_fresnel(plot=False, npix=128): """ Test that tilt with increasing WFE towards the +X direction moves the PSF in the -X direction Fresnel propagation version See also test_core.test_source_offsets_in_OpticalSystem """ # Create a wavefront and apply a tilt wave = poppy.FresnelWavefront(beam_radius=0.5 * u.m, npix=npix, oversample=8) wave *= poppy.CircularAperture(radius=0.5 * u.m) # tilt in arcseconds tilt_angle = -0.2 # must be a negative number (for -X direction shift), and within the FOV wave.tilt( Xangle=tilt_angle ) # for this function, the input is the desired direction for the image to tilt. # A shift to -X is implemented by creating an OPD that increases toward +X n = wave.shape[0] assert wave.wfe[n // 2, n // 2 - 5] < wave.wfe[n // 2, n // 2 + 5], "Wavefront error should increase to +X" if plot: plt.suptitle("Wavefront tilt sign test (Fresnel propagation)", fontweight='bold') wave.display(what='both') focal_length = 1 * u.m wave *= poppy.QuadraticLens(f_lens=focal_length) wave.propagate_fresnel(focal_length) if plot: plt.figure() wave.display(what='both', crosshairs=True, imagecrop=0.00001, scale='log') n = wave.shape[0] nominal_cen = n // 2 # In Fresnel mode, PSFs are centered on a pixel by default # (different from in Fraunhofer mode by half a pixel) cen = poppy.measure_centroid(wave.as_fits()) assert np.allclose( cen[0], nominal_cen), "Tilt in X should not displace the PSF in Y" assert cen[ 1] < nominal_cen, "WFE tilt increasing to +X should displace the PSF to -X" assert np.allclose( ((cen[1] - nominal_cen) * u.pixel * wave.pixelscale).to_value(u.m), ((tilt_angle * u.arcsec).to_value(u.radian) * focal_length).to_value( u.m)), "PSF offset distance did not match expected amount"
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 test_wavefront_tilt_sign_and_direction(plot=False, npix=128): """ Test that tilt with increasing WFE towards the +X direction moves the PSF in the -X direction Fraunhofer propagation version See also test_core.test_source_offsets_in_OpticalSystem """ # Create a wavefront and apply a tilt wave = poppy.Wavefront(diam=1 * u.m, npix=npix) wave *= poppy.CircularAperture(radius=0.5 * u.m) tilt_angle = -0.2 # must be a negative number (for -X direction shift), and within the FOV wave.tilt( Xangle=tilt_angle ) # for this function, the input is the desired direction for the image to tilt. # A shift to -X is implemented by creating an OPD that increases toward +X n = wave.shape[0] assert wave.wfe[n // 2, n // 2 - 5] < wave.wfe[n // 2, n // 2 + 5], "Wavefront error should increase to +X" if plot: plt.suptitle("Wavefront tilt sign test (Fraunhofer propagation)", fontweight='bold') wave.display(what='both') wave.propagate_to(poppy.Detector(pixelscale=0.05, fov_pixels=128)) if plot: plt.figure() wave.display(what='both', crosshairs=True, imagecrop=2) n = wave.shape[0] cen = poppy.measure_centroid(wave.as_fits()) assert np.allclose(cen[0], (n - 1) / 2), "Tilt in X should not displace the PSF in Y" assert cen[1] < ( n - 1) / 2, "WFE tilt increasing to +X should displace the PSF to -X" assert np.allclose(((cen[1] - (n - 1) / 2) * u.pixel * wave.pixelscale).to_value(u.arcsec), tilt_angle), "PSF offset did not match expected amount"
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 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 test_source_offsets_in_OpticalSystem(npix=128, fov_size=1, verbose=False): """Test source offsets within the field move in the expected directions and by the expected amounts The source offset positions are specified in the *output* detector coordinate frame, (i.e. for where the PSF should appear in the output image), but we create the wavefront in the entrance pupil coordinate frame. These may be different if there are coordinate transforms for axes flips or rotations. Therefore test several cases and ensure the output PSF appears in the expected location in each case. Parameters: ---------- npix : int number of pixels fov_size : fov size in arcsec (pretty much arbitrary) """ if npix < 110: raise ValueError( "npix < 110 results in too few pixels for fwcentroid to work properly." ) pixscale = fov_size / npix center_coords = np.asarray((npix - 1, npix - 1)) / 2 ref_psf1 = None # below we will save and compare PSFs with transforms to one without. for transform in ['no', 'inversion', 'rotation', 'both']: osys = poppy.OpticalSystem(oversample=1, npix=npix) osys.add_pupil(poppy.CircularAperture(radius=1.0)) if transform == 'inversion' or transform == 'both': if verbose: print("ADD INVERSION") osys.add_inversion(axis='y') if transform == 'rotation' or transform == 'both': if verbose: print("ADD ROTATION") osys.add_rotation(angle=12.5) osys.add_detector(pixelscale=pixscale, fov_pixels=npix) # a PSF with no offset should be centered psf0 = osys.calc_psf() cen = poppy.measure_centroid(psf0) assert np.allclose( cen, center_coords), "PSF with no source offset should be centered" if verbose: print( f"PSF with no offset OK for system with {transform} transform.\n" ) # Compute a PSF with the source offset towards PA=0 (+Y), still within the FOV osys.source_offset_r = 0.3 * fov_size # Shift to PA=0 should move in +Y osys.source_offset_theta = 0 psf1 = osys.calc_psf() cen = poppy.measure_centroid(psf1) assert np.allclose( (cen[0] - center_coords[0]) * pixscale, osys.source_offset_r, rtol=0.1), "Measured centroid in Y did not match expected offset" assert np.allclose( cen[1], center_coords[1], rtol=0.1 ), "Measured centroid in X should not shift for this test case" if verbose: print( f"PSF with +Y offset OK for system with {transform} transform.\n" ) if ref_psf1 is None: ref_psf1 = psf1 else: assert np.allclose( ref_psf1[0].data, psf1[0].data, atol=1e-4 ), "PSF is inconsistent with the system without any transforms" # Shift to PA=90 should move in -X osys.source_offset_theta = 90 psf2 = osys.calc_psf() cen = poppy.measure_centroid(psf2) assert np.allclose( (cen[1] - center_coords[1]) * pixscale, -osys.source_offset_r, rtol=0.1), "Measured centroid in X did not match expected offset" assert np.allclose( cen[0], center_coords[0], rtol=0.1 ), "Measured centroid in Y should not shift for this test case" if verbose: print( f"PSF with -X offset OK for system with {transform} transform.\n" )