def getPSF(SCAs=None, approximate_struts=False, n_waves=None, extra_aberrations=None, wavelength_limits=None, logger=None, wavelength=None, high_accuracy=False, gsparams=None): """ Get the PSF for WFIRST observations. By default, this routine returns a dict of ChromaticOpticalPSF objects, with the dict indexed by the SCA (Sensor Chip Array, the equivalent of a chip in an optical CCD). The PSF for a given SCA corresponds to that for the center of the SCA. Currently we do not use information about PSF variation within each SCA, which is relatively small. This routine also takes an optional keyword `SCAs`, which can be a single number or an iterable; if this is specified then results are not included for the other SCAs. The default is to do the calculations using the full specification of the WFIRST pupil plane, which is a costly calculation in terms of memory. For this, we use the provided pupil plane for red bands from http://wfirst.gsfc.nasa.gov/science/sdt_public/wps/references/instrument/ (Cycle 5) and we neglect for now the fact that the pupil plane configuration is slightly different for imaging in Z087, Y106, J129. To avoid using the full pupil plane configuration, use the optional keyword `approximate_struts`. In this case, the pupil plane will have the correct obscuration and number of struts, but the struts will be purely radial and evenly spaced instead of the true configuration. The simplicity of this arrangement leads to a much faster calculation, and somewhat simplifies the configuration of the diffraction spikes. Also note that currently the orientation of the struts is fixed, rather than rotating depending on the orientation of the focal plane. Rotation of the PSF can easily be affected by the user via psf = galsim.wfirst.getPSF(...).rotate(angle) which will rotate the entire PSF (including the diffraction spikes and any other features). The calculation takes advantage of the fact that the diffraction limit and aberrations have a simple, understood wavelength-dependence. (The WFIRST project webpage for Cycle 5 does in fact provide aberrations as a function of wavelength, but the deviation from the expected chromatic dependence is very small and we neglect it here.) For reference, the script use to parse the Zernikes given on the webpage and create the files in the GalSim repository can be found in `devel/external/parse_wfirst_zernikes_0715.py`. The resulting chromatic object can be used to draw into any of the WFIRST bandpasses. For applications that require very high accuracy in the modeling of the PSF, with very limited aliasing, the `high_accuracy` option can be set to True. When using this option, the MTF has a value below 1e-4 for all wavenumbers above the band limit when using `approximate_struts=True`, or below 3e-4 when using `approximate_struts=False`. In contrast, when `high_accuracy=False` (the default), there are some bumps in the MTF above the band limit that reach an amplitude of ~1e-2. By default, no additional aberrations are included above the basic design. However, users can provide an optional keyword `extra_aberrations` that will be included on top of those that are part of the design. This should be in the same format as for the ChromaticOpticalPSF class, with units of waves at the fiducial wavelength, 1293 nm. Currently, only aberrations up to order 11 (Noll convention) can be simulated. For WFIRST, the current tolerance for additional aberrations is a total of 90 nanometers RMS: http://wfirst.gsfc.nasa.gov/science/sdt_public/wps/references/instrument/README_AFTA_C5_WFC_Zernike_and_Field_Data.pdf distributed largely among coma, astigmatism, trefoil, and spherical aberrations (NOT defocus). This information might serve as a guide for reasonable `extra_aberrations` inputs. Jitter and charge diffusion are, by default, not included. Users who wish to include these can find some guidelines for typical length scales of the Gaussians that can represent these effects, and convolve the ChromaticOpticalPSF with appropriate achromatic Gaussians. The PSFs are always defined assuming the user will specify length scales in arcsec. @param SCAs Specific SCAs for which the PSF should be loaded. This can be either a single number or an iterable. If None, then the PSF will be loaded for all SCAs (1...18). Note that the object that is returned is a dict indexed by the requested SCA indices. [default: None] @param approximate_struts Should the routine use an approximate representation of the pupil plane, with 6 equally-spaced radial struts, instead of the exact representation of the pupil plane? Setting this parameter to True will lead to faster calculations, with a slightly less realistic PSFs. [default: False] @param n_waves Number of wavelengths to use for setting up interpolation of the chromatic PSF objects, which can lead to much faster image rendering. If None, then no interpolation is used. Note that users who want to interpolate can always set up the interpolation later on even if they do not do so when calling getPSF(). [default: None] @param extra_aberrations Array of extra aberrations to include in the PSF model, on top of those that are part of the WFIRST design. These should be provided in units of waves at the fiducial wavelength of 1293 nm, as an array of length 12 with entries 4 through 11 corresponding to defocus through spherical aberrations. [default: None] @param wavelength_limits A tuple or list of the blue and red wavelength limits to use for interpolating the chromatic object, if `n_waves` is not None. If None, then it uses the blue and red limits of all imaging passbands to determine the most inclusive wavelength range possible. But this keyword can be used to reduce the range of wavelengths if only one passband (or a subset of passbands) is to be used for making the images. [default: None] @param logger A logger object for output of progress statements if the user wants them. [default: None] @param wavelength An option to get an achromatic PSF for a single wavelength, for users who do not care about chromaticity of the PSF. If None, then the fully chromatic PSF is returned. Alternatively the user should supply either (a) a wavelength in nanometers, and they will get achromatic OpticalPSF objects for that wavelength, or (b) a bandpass object, in which case they will get achromatic OpticalPSF objects defined at the effective wavelength of that bandpass. [default: False] @param high_accuracy If True, make higher-fidelity representations of the PSF in Fourier space, to minimize aliasing (see plots on https://github.com/GalSim-developers/GalSim/issues/661 for more details). This setting is more expensive in terms of time and RAM, and may not be necessary for many applications. [default: False] @param gsparams An optional GSParams argument. See the docstring for GSParams for details. [default: None] @returns A dict of ChromaticOpticalPSF or OpticalPSF objects for each SCA. """ # Check which SCAs are to be done using a helper routine in this module. SCAs = galsim.wfirst._parse_SCAs(SCAs) # Deal with some accuracy settings. if high_accuracy: if approximate_struts: oversampling = 3.5 else: oversampling = 2.0 # In this case, we need to pad the edges of the pupil plane image, so we cannot just use # the stored file. tmp_pupil_plane_im = galsim.fits.read( galsim.wfirst.pupil_plane_file) old_bounds = tmp_pupil_plane_im.bounds new_bounds = old_bounds.withBorder( (old_bounds.xmax + 1 - old_bounds.xmin) / 2) pupil_plane_im = galsim.Image(bounds=new_bounds) pupil_plane_im[old_bounds] = tmp_pupil_plane_im pupil_plane_scale = galsim.wfirst.pupil_plane_scale else: if approximate_struts: oversampling = 1.5 else: oversampling = 1.2 pupil_plane_im = galsim.wfirst.pupil_plane_file pupil_plane_scale = galsim.wfirst.pupil_plane_scale if wavelength is None: if n_waves is not None: if wavelength_limits is None: # To decide the range of wavelengths to use (if none were passed in by the user), # first check out all the bandpasses. bandpass_dict = galsim.wfirst.getBandpasses() # Then find the blue and red limit to be used for the imaging bandpasses overall. blue_limit, red_limit = _find_limits(default_bandpass_list, bandpass_dict) else: if not isinstance(wavelength_limits, tuple): raise ValueError( "Wavelength limits must be entered as a tuple!") blue_limit, red_limit = wavelength_limits if red_limit <= blue_limit: raise ValueError( "Wavelength limits must have red_limit > blue_limit." "Input: blue limit=%f, red limit=%f nanometers" % (blue_limit, red_limit)) else: if isinstance(wavelength, galsim.Bandpass): wavelength_nm = wavelength.effective_wavelength elif isinstance(wavelength, float): wavelength_nm = wavelength else: raise TypeError( "Keyword 'wavelength' should either be a Bandpass, float," " or None.") # Start reading in the aberrations for the relevant SCAs. aberration_dict = {} PSF_dict = {} if logger: logger.debug('Beginning to loop over SCAs and get the PSF:') for SCA in SCAs: aberration_dict[SCA] = _read_aberrations(SCA) use_aberrations = aberration_dict[SCA] if extra_aberrations is not None: use_aberrations += extra_aberrations # We don't want to use piston, tip, or tilt aberrations. The former doesn't affect the # appearance of the PSF, and the latter cause centroid shifts. So, we set the first 4 # numbers (corresponding to a place-holder, piston, tip, and tilt) to zero. use_aberrations[0:4] = 0. # Now set up the PSF for this SCA, including the option to simplify the pupil plane. if logger: logger.debug(' ... SCA %d' % SCA) if wavelength is None: if approximate_struts: PSF = galsim.ChromaticOpticalPSF( lam=zemax_wavelength, diam=galsim.wfirst.diameter, aberrations=use_aberrations, obscuration=galsim.wfirst.obscuration, nstruts=6, oversampling=oversampling, gsparams=gsparams) else: PSF = galsim.ChromaticOpticalPSF( lam=zemax_wavelength, diam=galsim.wfirst.diameter, aberrations=use_aberrations, obscuration=galsim.wfirst.obscuration, pupil_plane_im=pupil_plane_im, pupil_plane_scale=pupil_plane_scale, oversampling=oversampling, pad_factor=2., gsparams=gsparams) if n_waves is not None: PSF = PSF.interpolate(waves=np.linspace( blue_limit, red_limit, n_waves), oversample_fac=1.5) else: tmp_aberrations = use_aberrations * zemax_wavelength / wavelength_nm if approximate_struts: PSF = galsim.OpticalPSF(lam=wavelength_nm, diam=galsim.wfirst.diameter, aberrations=tmp_aberrations, obscuration=galsim.wfirst.obscuration, nstruts=6, oversampling=oversampling, gsparams=gsparams) else: PSF = galsim.OpticalPSF(lam=wavelength_nm, diam=galsim.wfirst.diameter, aberrations=tmp_aberrations, obscuration=galsim.wfirst.obscuration, pupil_plane_im=pupil_plane_im, pupil_plane_scale=pupil_plane_scale, oversampling=oversampling, pad_factor=2., gsparams=gsparams) PSF_dict[SCA] = PSF return PSF_dict
def _get_single_PSF(SCA, bandpass, SCA_pos, approximate_struts, n_waves, extra_aberrations, logger, wavelength, high_accuracy, pupil_plane_type, gsparams): """Routine for making a single PSF. This gets called by getPSF() after it parses all the options that were passed in. Users will not directly interact with this routine. """ # Deal with some accuracy settings. if high_accuracy: if approximate_struts: oversampling = 3.5 else: oversampling = 2.0 # In this case, we need to pad the edges of the pupil plane image, so we cannot just use # the stored file. if pupil_plane_type == 'long': tmp_pupil_plane_im = galsim.fits.read( galsim.wfirst.pupil_plane_file_longwave) else: tmp_pupil_plane_im = galsim.fits.read( galsim.wfirst.pupil_plane_file_shortwave) old_bounds = tmp_pupil_plane_im.bounds new_bounds = old_bounds.withBorder( (old_bounds.xmax + 1 - old_bounds.xmin) / 2) pupil_plane_im = galsim.Image(bounds=new_bounds) pupil_plane_im[old_bounds] = tmp_pupil_plane_im pupil_plane_scale = galsim.wfirst.pupil_plane_scale else: if approximate_struts: oversampling = 1.5 else: oversampling = 1.2 if pupil_plane_type == 'long': pupil_plane_im = galsim.wfirst.pupil_plane_file_longwave else: pupil_plane_im = galsim.wfirst.pupil_plane_file_shortwave pupil_plane_scale = galsim.wfirst.pupil_plane_scale # Start reading in the aberrations for that SCA if logger: logger.debug('Beginning to get the PSF aberrations for SCA %d.' % SCA) aberrations, x_pos, y_pos = _read_aberrations(SCA) # Do bilinear interpolation, unless we're exactly at the center (default). use_aberrations = _interp_aberrations_bilinear(aberrations, x_pos, y_pos, SCA_pos) if extra_aberrations is not None: use_aberrations += extra_aberrations # We don't want to use piston, tip, or tilt aberrations. The former doesn't affect the # appearance of the PSF, and the latter cause centroid shifts. So, we set the first 4 # numbers (corresponding to a place-holder, piston, tip, and tilt) to zero. use_aberrations[0:4] = 0. # Now set up the PSF, including the option to simplify the pupil plane. if wavelength is None: if approximate_struts: PSF = galsim.ChromaticOpticalPSF( lam=zemax_wavelength, diam=galsim.wfirst.diameter, aberrations=use_aberrations, obscuration=galsim.wfirst.obscuration, nstruts=6, oversampling=oversampling, gsparams=gsparams) else: PSF = galsim.ChromaticOpticalPSF( lam=zemax_wavelength, diam=galsim.wfirst.diameter, aberrations=use_aberrations, obscuration=galsim.wfirst.obscuration, pupil_plane_im=pupil_plane_im, pupil_plane_scale=pupil_plane_scale, oversampling=oversampling, pad_factor=2., gsparams=gsparams) if n_waves is not None: # To decide the range of wavelengths to use, check the bandpass. bp_dict = galsim.wfirst.getBandpasses() bp = bp_dict[bandpass] PSF = PSF.interpolate(waves=np.linspace(bp.blue_limit, bp.red_limit, n_waves), oversample_fac=1.5) else: if not isinstance(wavelength, float): raise TypeError( "wavelength should either be a Bandpass, float, or None.") tmp_aberrations = use_aberrations * zemax_wavelength / wavelength if approximate_struts: PSF = galsim.OpticalPSF(lam=wavelength, diam=galsim.wfirst.diameter, aberrations=tmp_aberrations, obscuration=galsim.wfirst.obscuration, nstruts=6, oversampling=oversampling, gsparams=gsparams) else: PSF = galsim.OpticalPSF(lam=wavelength, diam=galsim.wfirst.diameter, aberrations=tmp_aberrations, obscuration=galsim.wfirst.obscuration, pupil_plane_im=pupil_plane_im, pupil_plane_scale=pupil_plane_scale, oversampling=oversampling, pad_factor=2., gsparams=gsparams) return PSF