def test_shear_units(): """Test that the shears we get out do not depend on the input PS and grid units.""" import time t1 = time.time() rand_seed = 123456 grid_size = 10. # degrees ngrid = 100 # Define a PS with some normalization value P(k=1/arcsec)=1 arcsec^2. # For this case we are getting the shears using units of arcsec for everything. ps = galsim.PowerSpectrum(lambda k: k) g1, g2 = ps.buildGrid(grid_spacing=3600. * grid_size / ngrid, ngrid=ngrid, rng=galsim.BaseDeviate(rand_seed)) # The above was done with all inputs given in arcsec. Now, redo it, inputting the PS # information in degrees and the grid info in arcsec. # We know that if k=1/arcsec, then when expressed as 1/degrees, it is # k=3600/degree. So define the PS as P(k=3600/degree)=(1/3600.)^2 degree^2. ps = galsim.PowerSpectrum(lambda k: (1. / 3600.**2) * (k / 3600.), units=galsim.degrees) g1_2, g2_2 = ps.buildGrid(grid_spacing=3600. * grid_size / ngrid, ngrid=ngrid, rng=galsim.BaseDeviate(rand_seed)) # Finally redo it, inputting the PS and grid info in degrees. ps = galsim.PowerSpectrum(lambda k: (1. / 3600.**2) * (k / 3600.), units=galsim.degrees) g1_3, g2_3 = ps.buildGrid(grid_spacing=grid_size / ngrid, ngrid=ngrid, units=galsim.degrees, rng=galsim.BaseDeviate(rand_seed)) # Since same random seed was used, require complete equality of shears, which would show that # all unit conversions were properly handled. np.testing.assert_array_almost_equal( g1, g1_2, decimal=9, err_msg='Incorrect unit handling in lensing engine') np.testing.assert_array_almost_equal( g1, g1_3, decimal=9, err_msg='Incorrect unit handling in lensing engine') np.testing.assert_array_almost_equal( g2, g2_2, decimal=9, err_msg='Incorrect unit handling in lensing engine') np.testing.assert_array_almost_equal( g2, g2_3, decimal=9, err_msg='Incorrect unit handling in lensing engine') t2 = time.time() print 'time for %s = %.2f' % (funcname(), t2 - t1)
def test_shear_seeds(): """Test that shears from lensing engine behave appropriate when given same/different seeds""" import time t1 = time.time() # make a power spectrum for some E, B power function test_ps = galsim.PowerSpectrum(e_power_function=pk2, b_power_function=pk2) # get shears on a grid w/o specifying seed g1, g2 = test_ps.buildGrid(grid_spacing=1.0, ngrid = 10) # do it again, w/o specifying seed: should differ g1new, g2new = test_ps.buildGrid(grid_spacing=1.0, ngrid = 10) assert not ((g1[0,0]==g1new[0,0]) or (g2[0,0]==g2new[0,0])) # get shears on a grid w/ specified seed g1, g2 = test_ps.buildGrid(grid_spacing=1.0, ngrid = 10, rng=galsim.BaseDeviate(13796)) # get shears on a grid w/ same specified seed: should be same g1new, g2new = test_ps.buildGrid(grid_spacing=1.0, ngrid = 10, rng=galsim.BaseDeviate(13796)) np.testing.assert_array_equal(g1, g1new, err_msg="New shear field differs from previous (same seed)!") np.testing.assert_array_equal(g2, g2new, err_msg="New shear field differs from previous (same seed)!") # get shears on a grid w/ diff't specified seed: should differ g1new, g2new = test_ps.buildGrid(grid_spacing=1.0, ngrid = 10, rng=galsim.BaseDeviate(1379)) assert not ((g1[0,0]==g1new[0,0]) or (g2[0,0]==g2new[0,0])) t2 = time.time() print 'time for %s = %.2f'%(funcname(),t2-t1)
def read_ps(galsim_dir=None, scale=1.): """Read in a Power Spectrum stored in the GalSim repository. Returns a galsim.PowerSpectrum object. """ import os if galsim_dir is None: raise ValueError( "You must supply a directory for your GalSim install via the `galsim_dir` kwarg." ) file = os.path.join(galsim_dir, 'examples', 'data', 'cosmo-fid.zmed1.00_smoothed.out') if scale == 1.: tab_ps = galsim.LookupTable(file=file, interpolant='linear') else: data = np.loadtxt(file).transpose() if data.shape[0] != 2: raise ValueError( "File %s provided for LookupTable does not have 2 columns" % file) x = data[0] f = data[1] f *= scale tab_ps = galsim.LookupTable(x=x, f=f, interpolant='linear') # Put this table into an E-mode power spectrum and return ret = galsim.PowerSpectrum(tab_ps, None, units=galsim.radians) return ret
def test_shear_reference(): """Test shears from lensing engine compared to stored reference values""" import time t1 = time.time() # read input data ref = np.loadtxt(refdir + '/shearfield_reference.dat') g1_in = ref[:,0] g2_in = ref[:,1] kappa_in = ref[:,2] # set up params rng = galsim.BaseDeviate(14136) n = 10 dx = 1. # define power spectrum ps = galsim.PowerSpectrum(e_power_function=lambda k : k**0.5, b_power_function=lambda k : k) # get shears g1, g2, kappa = ps.buildGrid(grid_spacing = dx, ngrid = n, rng=rng, get_convergence=True) # put in same format as data that got read in g1vec = g1.reshape(n*n) g2vec = g2.reshape(n*n) kappavec = kappa.reshape(n*n) # compare input vs. calculated values np.testing.assert_almost_equal(g1_in, g1vec, 9, err_msg = "Shear field differs from reference shear field!") np.testing.assert_almost_equal(g2_in, g2vec, 9, err_msg = "Shear field differs from reference shear field!") np.testing.assert_almost_equal(kappa_in, kappavec, 9, err_msg = "Convergence differences from references!") t2 = time.time() print 'time for %s = %.2f'%(funcname(),t2-t1)
def simulate_shear(constants, redshift, nbins, noise_sd=0.0, seed=0): """Takes cosmological parameters, generates a shear map, and adds noise. Inputs: constants: Constants, object for cosmological constants redshift: float, the redshift value for the sample nbins: int, the number of bins for the correlation function noise_sd: float, the standard deviation for IID Gaussian noise. seed: integer, seed for the RNG; if 0 it uses a randomly chosen seed. Returns: A pair of shear spectrum grids. """ grid_nx = 100 # length of grid in one dimension (degrees) theta = 10.0 # grid spacing dtheta = theta / grid_nx # wavenumbers at which to evaluate power spectra ell = np.logspace(-2.0, 4.0, num=50) nicaea_obj = constants.nicaea_object() psObs_nicaea = nicaea_obj.convergencePowerSpectrum(ell=ell, z=redshift) psObs_tabulated = galsim.LookupTable(ell, psObs_nicaea, interpolant='linear') ps_galsim = galsim.PowerSpectrum(psObs_tabulated, delta2=False, units=galsim.radians) grid_deviate = galsim.BaseDeviate(seed) g1, g2, kappa = ps_galsim.buildGrid(grid_spacing=dtheta, ngrid=grid_nx, rng=grid_deviate, units='degrees', kmin_factor=2, kmax_factor=2, get_convergence=True) g1_r, g2_r, _ = galsim.lensing_ps.theoryToObserved(g1, g2, kappa) rng = galsim.GaussianDeviate(seed=seed) g1_noise_grid = galsim.ImageD(grid_nx, grid_nx) g2_noise_grid = galsim.ImageD(grid_nx, grid_nx) g1_noise_grid.addNoise(galsim.GaussianNoise(rng=rng, sigma=noise_sd)) g2_noise_grid.addNoise(galsim.GaussianNoise(rng=rng, sigma=noise_sd)) g1_noisy = np.add(g1_r, g1_noise_grid.array) g2_noisy = np.add(g2_r, g2_noise_grid.array) min_sep = dtheta max_sep = grid_nx * np.sqrt(2) * dtheta grid_range = dtheta * np.arange(grid_nx) x, y = np.meshgrid(grid_range, grid_range) stats = run_treecorr(x, y, g1, g2, min_sep, max_sep, nbins=nbins) return g1_noisy, g2_noisy, stats
def test_shear_get(): """Check that using gridded outputs and the various getFoo methods gives consistent results""" import time t1 = time.time() # choose a power spectrum and grid setup my_ps = galsim.PowerSpectrum(lambda k : k**0.5) # build the grid grid_spacing = 1. ngrid = 100 g1, g2, kappa = my_ps.buildGrid(grid_spacing = grid_spacing, ngrid = ngrid, get_convergence = True) min = (-ngrid/2 + 0.5) * grid_spacing max = (ngrid/2 - 0.5) * grid_spacing x, y = np.meshgrid(np.arange(min,max+grid_spacing,grid_spacing), np.arange(min,max+grid_spacing,grid_spacing)) # convert theoretical to observed quantities for grid g1_r, g2_r, mu = galsim.lensing_ps.theoryToObserved(g1, g2, kappa) # use getShear, getConvergence, getMagnification, getLensing do appropriate consistency checks test_g1_r, test_g2_r = my_ps.getShear((x.flatten(), y.flatten())) test_g1, test_g2 = my_ps.getShear((x.flatten(), y.flatten()), reduced = False) test_kappa = my_ps.getConvergence((x.flatten(), y.flatten())) test_mu = my_ps.getMagnification((x.flatten(), y.flatten())) test_g1_r_2, test_g2_r_2, test_mu_2 = my_ps.getLensing((x.flatten(), y.flatten())) np.testing.assert_almost_equal(g1.flatten(), test_g1, 9, err_msg="Shears from grid and getShear disagree!") np.testing.assert_almost_equal(g2.flatten(), test_g2, 9, err_msg="Shears from grid and getShear disagree!") np.testing.assert_almost_equal(g1_r.flatten(), test_g1_r, 9, err_msg="Reduced shears from grid and getShear disagree!") np.testing.assert_almost_equal(g2_r.flatten(), test_g2_r, 9, err_msg="Reduced shears from grid and getShear disagree!") np.testing.assert_almost_equal(g1_r.flatten(), test_g1_r_2, 9, err_msg="Reduced shears from grid and getLensing disagree!") np.testing.assert_almost_equal(g2_r.flatten(), test_g2_r_2, 9, err_msg="Reduced shears from grid and getLensing disagree!") np.testing.assert_almost_equal(kappa.flatten(), test_kappa, 9, err_msg="Convergences from grid and getConvergence disagree!") np.testing.assert_almost_equal(mu.flatten(), test_mu, 9, err_msg="Magnifications from grid and getMagnification disagree!") np.testing.assert_almost_equal(mu.flatten(), test_mu_2, 9, err_msg="Magnifications from grid and getLensing disagree!") t2 = time.time() print 'time for %s = %.2f'%(funcname(),t2-t1)
def generate_bmode_shears(var, ngrid, rng=None, kmax_factor=16, kmin_factor=1): """Generate b-mode shears, returns: g1, g2, gmag, gphi """ ps = galsim.PowerSpectrum(b_power_function=lambda karr: var * np.ones_like( karr) / float(kmax_factor)**2) g1, g2 = ps.buildGrid(grid_spacing=1., ngrid=ngrid, rng=rng, kmax_factor=kmax_factor, kmin_factor=kmin_factor) # These g1, g2 *shears* do not have to be on the unit disc, so we have to convert them to a |g| # like ellipticity via the result for a circle following such a shear, using Schneider (2006) # eq. 12. Note, this itself will mean that the ellips are not pure B-mode!! gmag = np.sqrt(g1 * g1 + g2 * g2) for i in range(g1.shape[0]): # ugly but my arm is broken for j in range(g2.shape[1]): if gmag[i, j] > 1.: g1[i, j] = g1[i, j] / gmag[i, j]**2 g2[i, j] = g2[i, j] / gmag[i, j]**2 gmag[i, j] = np.sqrt(g1[i, j]**2 + g2[i, j]**2) else: pass gphi = .5 * np.arctan2(g2, g1) return g1, g2, gmag, gphi
def __init__(self, *, rng, im_width, buff, scale, trunc=1, variation_factor=10, fwhm=0.8): self._rng = rng self._im_cen = (im_width - 1) / 2 self._scale = scale self._tot_width = im_width + 2 * buff self._x_scale = 2.0 / self._tot_width / scale self._buff = buff self._variation_factor = variation_factor self._median_seeing = fwhm # set the power spectrum and PSF params # Heymans et al, 2012 found L0 ~= 3 arcmin, given as 180 arcsec here. def _pf(k): return (k**2 + (1. / 180)**2)**(-11. / 6.) * np.exp(-(k * trunc)**2) self._ps = galsim.PowerSpectrum(e_power_function=_pf, b_power_function=_pf) ng = 128 gs = max(self._tot_width * self._scale / ng, 1) self.ng = ng self.gs = gs seed = self._rng.randint(1, 2**30) self._ps.buildGrid(grid_spacing=gs, ngrid=ng, get_convergence=True, variance=(0.01 * variation_factor)**2, rng=galsim.BaseDeviate(seed)) # cache the galsim LookupTable2D objects by hand to speed computations g1_grid, g2_grid, mu_grid = galsim.lensing_ps.theoryToObserved( self._ps.im_g1.array, self._ps.im_g2.array, self._ps.im_kappa.array) self._lut_g1 = galsim.table.LookupTable2D( self._ps.x_grid, self._ps.y_grid, g1_grid.T, edge_mode='wrap', interpolant=galsim.Lanczos(5)) self._lut_g2 = galsim.table.LookupTable2D( self._ps.x_grid, self._ps.y_grid, g2_grid.T, edge_mode='wrap', interpolant=galsim.Lanczos(5)) self._lut_mu = galsim.table.LookupTable2D( self._ps.x_grid, self._ps.y_grid, mu_grid.T - 1, edge_mode='wrap', interpolant=galsim.Lanczos(5)) self._g1_mean = self._rng.normal() * 0.01 * variation_factor self._g2_mean = self._rng.normal() * 0.01 * variation_factor def _getlogmnsigma(mean, sigma): logmean = np.log(mean) - 0.5 * np.log(1 + sigma**2 / mean**2) logvar = np.log(1 + sigma**2 / mean**2) logsigma = np.sqrt(logvar) return logmean, logsigma lm, ls = _getlogmnsigma(self._median_seeing, 0.1) self._fwhm_central = np.exp(self._rng.normal() * ls + lm)
def main(argv): """ Make images using constant PSF and variable shear: - The main image is 2048 x 2048 pixels. - Pixel scale is 0.2 arcsec/pixel, hence the image is about 0.11 degrees on a side. - Applied shear is from a cosmological power spectrum read in from file. - The PSF is a real one from SDSS, and corresponds to a convolution of atmospheric PSF, optical PSF, and pixel response, which has been sampled at pixel centers. We used a PSF from SDSS in order to have a PSF profile that could correspond to what you see with a real telescope. However, in order that the galaxy resolution not be too poor, we tell GalSim that the pixel scale for that PSF image is 0.2" rather than 0.396". We are simultaneously lying about the intrinsic size of the PSF and about the pixel scale when we do this. - The galaxies come from COSMOSCatalog, which can produce either RealGalaxy profiles (like in demo10) and parametric fits to those profiles. We choose 30% of the galaxies to use the images, and the other 60% to use the parametric fits - The real galaxy images include some initial correlated noise from the original HST observation. However, we whiten the noise of the final image so the final image has stationary Gaussian noise, rather than correlated noise. """ logging.basicConfig(format="%(message)s", level=logging.INFO, stream=sys.stdout) logger = logging.getLogger("demo11") # Define some parameters we'll use below. # Normally these would be read in from some parameter file. pixel_scale = 0.2 # arcsec/pixel image_size = 2048 # size of image in pixels image_size_arcsec = image_size * pixel_scale # size of big image in each dimension (arcsec) noise_variance = 5.e4 # ADU^2 (Just use simple Gaussian noise here.) nobj = 288 # number of galaxies in entire field # (This corresponds to 8 galaxies / arcmin^2) grid_spacing = 90.0 # The spacing between the samples for the power spectrum # realization (arcsec) tel_diam = 4 # Let's figure out the flux for a 4 m class telescope exp_time = 300 # exposing for 300 seconds. center_ra = 19.3 * galsim.hours # The RA, Dec of the center of the image on the sky center_dec = -33.1 * galsim.degrees # The catalog returns objects that are appropriate for HST in 1 second exposures. So for our # telescope we scale up by the relative area and exposure time. Note that what is important is # the *effective* area after taking into account obscuration. For HST, the telescope diameter # is 2.4 but there is obscuration (a linear factor of 0.33). Here, we assume that the telescope # we're simulating effectively has no obscuration factor. We're also ignoring the pi/4 factor # since it appears in the numerator and denominator, so we use area = diam^2. hst_eff_area = 2.4**2 * (1. - 0.33**2) flux_scaling = (tel_diam**2 / hst_eff_area) * exp_time # random_seed is used for both the power spectrum realization and the random properties # of the galaxies. random_seed = 24783923 file_name = os.path.join('output', 'tabulated_power_spectrum.fits.fz') logger.info('Starting demo script 11') # Read in galaxy catalog # The COSMOSCatalog uses the same input file as we have been using for RealGalaxyCatalogs # along with a second file called real_galaxy_catalog_23.5_examples_fits.fits, which stores # the information about the parameteric fits. There is no need to specify the second file # name, since the name is derivable from the name of the main catalog. if True: # The catalog we distribute with the GalSim code only has 100 galaxies. # The galaxies will typically be reused several times here. cat_file_name = 'real_galaxy_catalog_23.5_example.fits' dir = 'data' cosmos_cat = galsim.COSMOSCatalog(cat_file_name, dir=dir) else: # If you've run galsim_download_cosmos, you can leave out the cat_file_name and dir # to use the full COSMOS catalog with 56,000 galaxies in it. cosmos_cat = galsim.COSMOSCatalog() logger.info('Read in %d galaxies from catalog', cosmos_cat.nobjects) # Setup the PowerSpectrum object we'll be using: # To do this, we first have to read in the tabulated shear power spectrum, often denoted # C_ell(ell), where ell has units of inverse angle and C_ell has units of angle^2. However, # GalSim works in the flat-sky approximation, so we use this notation interchangeably with # P(k). GalSim does not calculate shear power spectra for users, who must be able to provide # their own (or use the examples in the repository). # # Here we use a tabulated power spectrum from iCosmo (http://icosmo.org), with the following # cosmological parameters and survey design: # H_0 = 70 km/s/Mpc # Omega_m = 0.25 # Omega_Lambda = 0.75 # w_0 = -1.0 # w_a = 0.0 # n_s = 0.96 # sigma_8 = 0.8 # Smith et al. prescription for the non-linear power spectrum. # Eisenstein & Hu transfer function with wiggles. # Default dN/dz with z_med = 1.0 # The file has, as required, just two columns which are k and P(k). However, iCosmo works in # terms of ell and C_ell; ell is inverse radians and C_ell in radians^2. Since GalSim tends to # work in terms of arcsec, we have to tell it that the inputs are radians^-1 so it can convert # to store in terms of arcsec^-1. pk_file = os.path.join('data', 'cosmo-fid.zmed1.00.out') ps = galsim.PowerSpectrum(pk_file, units=galsim.radians) # The argument here is "e_power_function" which defines the E-mode power to use. logger.info('Set up power spectrum from tabulated P(k)') # Now let's read in the PSF. It's a real SDSS PSF, which means pixel scale of 0.396". However, # the typical seeing is 1.2" and we want to simulate better seeing, so we will just tell GalSim # that the pixel scale is 0.2". We have to be careful with SDSS PSF images, as they have an # added 'soft bias' of 1000 which has been removed before creation of this file, so that the sky # level is properly zero. Also, the file is bzipped, to demonstrate the ability of GalSim # handle this kind of compressed file (among others). We read the image directly into an # InterpolatedImage GSObject, so we can manipulate it as needed (here, the only manipulation # needed is convolution). The flux is 1 as needed for a PSF. psf_file = os.path.join('data', 'example_sdss_psf_sky0.fits.bz2') psf = galsim.InterpolatedImage(psf_file, scale=pixel_scale, flux=1.) logger.info('Read in PSF image from bzipped FITS file') # Setup the image: full_image = galsim.ImageF(image_size, image_size) # The default convention for indexing an image is to follow the FITS standard where the # lower-left pixel is called (1,1). However, this can be counter-intuitive to people more # used to C or python indexing, where indices start at 0. It is possible to change the # coordinates of the lower-left pixel with the methods `setOrigin`. For this demo, we # switch to 0-based indexing, so the lower-left pixel will be called (0,0). full_image.setOrigin(0, 0) # As for demo10, we use random_seed for the random numbers required for the # whole image. In this case, both the power spectrum realization and the noise on the # full image we apply later. rng = galsim.BaseDeviate(random_seed) # We want to make random positions within our image. However, currently for shears from a power # spectrum we first have to get shears on a grid of positions, and then we can choose random # positions within that. So, let's make the grid. We're going to make it as large as the # image, with grid points spaced by 90 arcsec (hence interpolation only happens below 90" # scales, below the interesting scales on which we want the shear power spectrum to be # represented exactly). The lensing engine wants positions in arcsec, so calculate that: ps.buildGrid(grid_spacing=grid_spacing, ngrid=int(math.ceil(image_size_arcsec / grid_spacing)), rng=rng) logger.info('Made gridded shears') # We keep track of how much noise is already in the image from the RealGalaxies. # The default initial value is all pixels = 0. noise_image = galsim.ImageF(image_size, image_size) noise_image.setOrigin(0, 0) # Make a slightly non-trivial WCS. We'll use a slightly rotated coordinate system # and center it at the image center. theta = 0.17 * galsim.degrees # ( dudx dudy ) = ( cos(theta) -sin(theta) ) * pixel_scale # ( dvdx dvdy ) ( sin(theta) cos(theta) ) # Aside: You can call numpy trig functions on Angle objects directly, rather than getting # their values in radians first. Or, if you prefer, you can write things like # theta.sin() or theta.cos(), which are equivalent. dudx = numpy.cos(theta) * pixel_scale dudy = -numpy.sin(theta) * pixel_scale dvdx = numpy.sin(theta) * pixel_scale dvdy = numpy.cos(theta) * pixel_scale image_center = full_image.true_center affine = galsim.AffineTransform(dudx, dudy, dvdx, dvdy, origin=full_image.true_center) # We can also put it on the celestial sphere to give it a bit more realism. # The TAN projection takes a (u,v) coordinate system on a tangent plane and projects # that plane onto the sky using a given point as the tangent point. The tangent # point should be given as a CelestialCoord. sky_center = galsim.CelestialCoord(ra=center_ra, dec=center_dec) # The third parameter, units, defaults to arcsec, but we make it explicit here. # It sets the angular units of the (u,v) intermediate coordinate system. wcs = galsim.TanWCS(affine, sky_center, units=galsim.arcsec) full_image.wcs = wcs # Now we need to loop over our objects: for k in range(nobj): time1 = time.time() # The usual random number generator using a different seed for each galaxy. ud = galsim.UniformDeviate(random_seed + k + 1) # Choose a random RA, Dec around the sky_center. # Note that for this to come out close to a square shape, we need to account for the # cos(dec) part of the metric: ds^2 = dr^2 + r^2 d(dec)^2 + r^2 cos^2(dec) d(ra)^2 # So need to calculate dec first. dec = center_dec + (ud() - 0.5) * image_size_arcsec * galsim.arcsec ra = center_ra + ( ud() - 0.5) * image_size_arcsec / numpy.cos(dec) * galsim.arcsec world_pos = galsim.CelestialCoord(ra, dec) # We will need the image position as well, so use the wcs to get that image_pos = wcs.toImage(world_pos) # We also need this in the tangent plane, which we call "world coordinates" here, # since the PowerSpectrum class is really defined on that plane, not in (ra,dec). uv_pos = affine.toWorld(image_pos) # Get the reduced shears and magnification at this point g1, g2, mu = ps.getLensing(pos=uv_pos) # Now we will have the COSMOSCatalog make a galaxy profile for us. It can make either # a RealGalaxy using the original HST image and PSF, or a parametric model based on # parametric fits to the light distribution of the HST observation. The parametric # models are either a Sersic fit to the data or a bulge + disk fit according to which # one gave the better chisq value. We will select a galaxy at random from the catalog. # One could easily do this by choosing an index = int(ud() * cosmos_cat.nobjects), but # we will instead allow the catalog to choose a random galaxy for us. It will remove any # selection effects involved in postage stamp creation using weights that are stored in # the catalog. (If for some reason you prefer not to do that, you can always choose a # purely random index yourself using int(ud() * cosmos_cat.nobjects).) We employ this # random selection by simply failing to specify an index or identifier for a galaxy, in # which case it chooses a random one. # First determine whether we will make a real galaxy (`gal_type = 'real'`) or a parametric # galaxy (`gal_type = 'parametric'`). The real galaxies take longer to render, so for this # script, we just use them 30% of the time and use parametric galaxies the other 70%. # We could just use `ud()<0.3` for this, but instead we introduce another Deviate type # available in GalSim that we haven't used yet: BinomialDeviate. # It takes an N and p value and returns integers according to a binomial distribution. # i.e. How many heads you get after N flips if each flip has a chance, p, of being heads. binom = galsim.BinomialDeviate(ud, N=1, p=0.3) real = binom() if real: # For real galaxies, we will want to whiten the noise in the image (below). # When whitening the image, we need to make sure the original correlated noise is # present throughout the whole image, otherwise the whitening will do the wrong thing # to the parts of the image that don't include the original image. The RealGalaxy # stores the correct noise profile to use as the gal.noise attribute. This noise # profile is automatically updated as we shear, dilate, convolve, etc. But we need to # tell it how large to pad with this noise by hand. This is a bit complicated for the # code to figure out on its own, so we have to supply the size for noise padding # with the noise_pad_size parameter. # The large galaxies will render fine without any noise padding, but the postage stamp # for the smaller galaxies will be sized appropriately for the PSF, which may make the # stamp larger than the original galaxy image. The psf image is 40 x 40, although # the bright part is much more concentrated than that. If we pad out the galaxy image # to at least 40 x sqrt(2), we should be safe even if the galaxy image is rotated # with respect to the psf image. # noise_pad_size = 40 * sqrt(2) * 0.2 arcsec/pixel = 11.3 arcsec gal = cosmos_cat.makeGalaxy(gal_type='real', rng=ud, noise_pad_size=11.3) else: gal = cosmos_cat.makeGalaxy(gal_type='parametric', rng=ud) # Apply a random rotation theta = ud() * 2.0 * numpy.pi * galsim.radians gal = gal.rotate(theta) # Rescale the flux to match our telescope configuration. # This automatically scales up the noise variance by flux_scaling**2. gal *= flux_scaling # Apply the cosmological (reduced) shear and magnification at this position using a single # GSObject method. gal = gal.lens(g1, g2, mu) # Convolve with the PSF. final = galsim.Convolve(psf, gal) # Account for the fractional part of the position # cf. demo9.py for an explanation of this nominal position stuff. x_nominal = image_pos.x + 0.5 y_nominal = image_pos.y + 0.5 ix_nominal = int(math.floor(x_nominal + 0.5)) iy_nominal = int(math.floor(y_nominal + 0.5)) dx = x_nominal - ix_nominal dy = y_nominal - iy_nominal offset = galsim.PositionD(dx, dy) # We use method='no_pixel' here because the SDSS PSF image that we are using includes the # pixel response already. stamp = final.drawImage(wcs=wcs.local(image_pos), offset=offset, method='no_pixel') # Recenter the stamp at the desired position: stamp.setCenter(ix_nominal, iy_nominal) # Find the overlapping bounds: bounds = stamp.bounds & full_image.bounds # Now, if we are using a real galaxy, we want to ether whiten or at least symmetrize the # noise on the postage stamp to avoid having to deal with correlated noise in any kind of # image processing you would want to do on the final image. (Like measure galaxy shapes.) # Galsim automatically propagates the noise correctly from the initial RealGalaxy object # through the applied shear, distortion, rotation, and convolution into the final object's # noise attribute. To make the noise fully white, use the image.whitenNoise() method. # The returned value is the variance of the Gaussian noise that is present after the # whitening process. # However, this is often overkill for many applications. If it is acceptable to merely end # up with noise with some degree of symmetry (say 4-fold or 8-fold symmetry), then you can # instead have GalSim just add enough noise to make the resulting noise have this kind of # symmetry. Usually this requires adding significantly less additional noise, which means # you can have the resulting total variance be somewhat smaller. The returned variance # corresponds to the zero-lag value of the noise correlation function, which will still have # off-diagonal elements. We can do this step using the image.symmetrizeNoise() method. if real: if True: # We use the symmetrizing option here. new_variance = stamp.symmetrizeNoise(final.noise, 8) else: # Here is how you would do it if you wanted to fully whiten the image. new_variance = stamp.whitenNoise(final.noise) # We need to keep track of how much variance we have currently in the image, so when # we add more noise, we can omit what is already there. noise_image[bounds] += new_variance # Finally, add the stamp to the full image. full_image[bounds] += stamp[bounds] time2 = time.time() tot_time = time2 - time1 logger.info('Galaxy %d: position relative to center = %s, t=%f s', k, str(uv_pos), tot_time) # We already have some noise in the image, but it isn't uniform. So the first thing to do is # to make the Gaussian noise uniform across the whole image. We have a special noise class # that can do this. VariableGaussianNoise takes an image of variance values and applies # Gaussian noise with the corresponding variance to each pixel. # So all we need to do is build an image with how much noise to add to each pixel to get us # up to the maximum value that we already have in the image. max_current_variance = numpy.max(noise_image.array) noise_image = max_current_variance - noise_image vn = galsim.VariableGaussianNoise(rng, noise_image) full_image.addNoise(vn) # Now max_current_variance is the noise level across the full image. We don't want to add that # twice, so subtract off this much from the intended noise that we want to end up in the image. noise_variance -= max_current_variance # Now add Gaussian noise with this variance to the final image. We have to do this step # at the end, rather than adding to individual postage stamps, in order to get the noise # level right in the overlap regions between postage stamps. noise = galsim.GaussianNoise(rng, sigma=math.sqrt(noise_variance)) full_image.addNoise(noise) logger.info('Added noise to final large image') # Now write the image to disk. It is automatically compressed with Rice compression, # since the filename we provide ends in .fz. full_image.write(file_name) logger.info('Wrote image to %r', file_name) # Compute some sky positions of some of the pixels to compare with the values of RA, Dec # that ds9 reports. ds9 always uses (1,1) for the lower left pixel, so the pixel coordinates # of these pixels are different by 1, but you can check that the RA and Dec values are # the same as what GalSim calculates. ra_str = center_ra.hms() dec_str = center_dec.dms() logger.info('Center of image is at RA %sh %sm %ss, DEC %sd %sm %ss', ra_str[0:3], ra_str[3:5], ra_str[5:], dec_str[0:3], dec_str[3:5], dec_str[5:]) for (x, y) in [(0, 0), (0, image_size - 1), (image_size - 1, 0), (image_size - 1, image_size - 1)]: world_pos = wcs.toWorld(galsim.PositionD(x, y)) ra_str = world_pos.ra.hms() dec_str = world_pos.dec.dms() logger.info('Pixel (%4d, %4d) is at RA %sh %sm %ss, DEC %sd %sm %ss', x, y, ra_str[0:3], ra_str[3:5], ra_str[5:], dec_str[0:3], dec_str[3:5], dec_str[5:]) logger.info( 'ds9 reports these pixels as (1,1), (1,2048), etc. with the same RA, Dec.' )
def test_PSE_basic(): """Basic test of power spectrum estimation. """ # Here are some parameters that define array sizes and other such things. array_size = 300 e_tolerance = 0.10 # 10% error allowed because of finite grid effects, noise fluctuations, # and other things. This unit test is just for a basic sanity test. b_tolerance = 0.15 # B-mode is slightly less accurate. zero_tolerance = 0.03 # For power that should be zero n_ell = 8 grid_spacing = 0.1 # degrees ps_file = os.path.join(datapath, 'cosmo-fid.zmed1.00.out') rand_seed = 2718 # Begin by setting up the PowerSpectrum and generating shears. tab = galsim.LookupTable.from_file(ps_file) ps = galsim.PowerSpectrum(tab, units=galsim.radians) g1, g2 = ps.buildGrid(grid_spacing=grid_spacing, ngrid=array_size, units=galsim.degrees, rng=galsim.BaseDeviate(rand_seed)) # Then initialize the PSE object. pse = galsim.pse.PowerSpectrumEstimator(N=array_size, sky_size_deg=array_size*grid_spacing, nbin=n_ell) do_pickle(pse) # Estimate the power spectrum using the PSE, without weighting. ell, P_e1, P_b1, P_eb1 = pse.estimate(g1, g2) # To check: P_E is right (to within the desired tolerance); P_B and P_EB are <1% of P_E. P_theory = np.zeros_like(ell) for ind in range(len(ell)): P_theory[ind] = tab(ell[ind]) # Note: we don't check the first element because at low ell the tests can fail more # spectacularly for reasons that are well understood. np.testing.assert_allclose(P_e1[1:], P_theory[1:], rtol=e_tolerance, err_msg='PSE returned wrong E power') np.testing.assert_allclose(P_b1[1:]/P_theory[1:], 0., atol=zero_tolerance, err_msg='PSE found B power') np.testing.assert_allclose(P_eb1[1:]/P_theory[1:], 0., atol=zero_tolerance, err_msg='PSE found EB cross-power') # Test theory_func ell, P_e1, P_b1, P_eb1, t = pse.estimate(g1, g2, theory_func=tab) # This isn't super accurate. I think just because of binning. But I'm not sure. np.testing.assert_allclose(t, P_theory, rtol=0.3, err_msg='PSE returned wrong theory binning') # Also check the case where P_e=P_b. ps = galsim.PowerSpectrum(tab, tab, units=galsim.radians) g1, g2 = ps.buildGrid(grid_spacing=grid_spacing, ngrid=array_size, units=galsim.degrees, rng=galsim.BaseDeviate(rand_seed)) ell, P_e2, P_b2, P_eb2 = pse.estimate(g1, g2) np.testing.assert_allclose(P_e2[1:], P_theory[1:], rtol=e_tolerance, err_msg='PSE returned wrong E power') np.testing.assert_allclose(P_b2[1:], P_theory[1:], rtol=b_tolerance, err_msg='PSE returned wrong B power') np.testing.assert_allclose(P_eb2[1:]/P_theory[1:], 0., atol=zero_tolerance, err_msg='PSE found EB cross-power') # And check the case where P_b is nonzero and P_e is zero. ps = galsim.PowerSpectrum(e_power_function=None, b_power_function=tab, units=galsim.radians) g1, g2 = ps.buildGrid(grid_spacing=grid_spacing, ngrid=array_size, units=galsim.degrees, rng=galsim.BaseDeviate(rand_seed)) ell, P_e3, P_b3, P_eb3 = pse.estimate(g1, g2) np.testing.assert_allclose(P_e3[1:]/P_theory[1:], 0., atol=zero_tolerance, err_msg='PSE found E power when it should be zero') np.testing.assert_allclose(P_b3[1:], P_theory[1:], rtol=b_tolerance, err_msg='PSE returned wrong B power') np.testing.assert_allclose(P_eb3[1:]/P_theory[1:], 0., atol=zero_tolerance, err_msg='PSE found EB cross-power') assert_raises(ValueError, pse.estimate, g1[:3,:3], g2) assert_raises(ValueError, pse.estimate, g1[:3,:8], g2[:3,:8]) assert_raises(ValueError, pse.estimate, g1[:8,:8], g2[:8,:8])
def test_PSE_weight(): """Test of power spectrum estimation with weights. """ array_size = 300 n_ell = 8 grid_spacing = 0.1 ps_file = os.path.join(datapath, 'cosmo-fid.zmed1.00.out') rand_seed = 2718 tab = galsim.LookupTable.from_file(ps_file) ps = galsim.PowerSpectrum(tab, units=galsim.radians) g1, g2 = ps.buildGrid(grid_spacing=grid_spacing, ngrid=array_size, units=galsim.degrees, rng=galsim.BaseDeviate(rand_seed)) pse = galsim.pse.PowerSpectrumEstimator(N=array_size, sky_size_deg=array_size*grid_spacing, nbin=n_ell) ell, P_e1, P_b1, P_eb1, P_theory = pse.estimate(g1, g2, weight_EE=True, weight_BB=True, theory_func=tab) print('P_e1 = ',P_e1) print('rel_diff = ',(P_e1-P_theory)/P_theory) print('rel_diff using P[1] = ',(P_e1-P_theory)/P_theory[1]) # The agreement here seems really bad. Should I not expect these to be closer than this? eb_tolerance = 0.4 zero_tolerance = 0.03 np.testing.assert_allclose(P_e1[1:], P_theory[1:], rtol=eb_tolerance, err_msg='Weighted PSE returned wrong E power') np.testing.assert_allclose(P_b1/P_theory, 0., atol=zero_tolerance, err_msg='Weighted PSE found B power') print(P_eb1/P_theory) np.testing.assert_allclose(P_eb1/P_theory, 0., atol=zero_tolerance, err_msg='Weighted PSE found EB cross-power') # Also check the case where P_e=P_b. ps = galsim.PowerSpectrum(tab, tab, units=galsim.radians) g1, g2 = ps.buildGrid(grid_spacing=grid_spacing, ngrid=array_size, units=galsim.degrees, rng=galsim.BaseDeviate(rand_seed)) ell, P_e2, P_b2, P_eb2 = pse.estimate(g1, g2, weight_EE=True, weight_BB=True) np.testing.assert_allclose(P_e2[1:], P_theory[1:], rtol=eb_tolerance, err_msg='Weighted PSE returned wrong E power') np.testing.assert_allclose(P_b2[1:], P_theory[1:], rtol=eb_tolerance, err_msg='Weighted PSE returned wrong B power') np.testing.assert_allclose(P_eb2[1:]/P_theory[1:], 0., atol=zero_tolerance, err_msg='Weighted PSE found EB cross-power') # And check the case where P_b is nonzero and P_e is zero. ps = galsim.PowerSpectrum(e_power_function=None, b_power_function=tab, units=galsim.radians) g1, g2 = ps.buildGrid(grid_spacing=grid_spacing, ngrid=array_size, units=galsim.degrees, rng=galsim.BaseDeviate(rand_seed)) ell, P_e3, P_b3, P_eb3 = pse.estimate(g1, g2, weight_EE=True, weight_BB=True) np.testing.assert_allclose(P_e3[1:]/P_theory[1:], 0., atol=zero_tolerance, err_msg='Weighted PSE found E power when it should be zero') np.testing.assert_allclose(P_b3[1:], P_theory[1:], rtol=eb_tolerance, err_msg='Weighted PSE returned wrong B power') np.testing.assert_allclose(P_eb3[1:]/P_theory[1:], 0., atol=zero_tolerance, err_msg='Weighted PSE found EB cross-power') assert_raises(TypeError, pse.estimate, g1, g2, weight_EE=8) assert_raises(TypeError, pse.estimate, g1, g2, weight_BB='yes') # If N is fairly small, then can get zeros in the counts, which raises an error array_size = 5 g1, g2 = ps.buildGrid(grid_spacing=grid_spacing, ngrid=array_size, units=galsim.degrees, rng=galsim.BaseDeviate(rand_seed)) pse = galsim.pse.PowerSpectrumEstimator(N=array_size, sky_size_deg=array_size*grid_spacing, nbin=n_ell) with assert_raises(galsim.GalSimError): pse.estimate(g1,g2)
def test_PSE_basic(): """Basic test of power spectrum estimation. """ t1 = time.time() # Begin by setting up the PowerSpectrum and generating shears. my_tab = galsim.LookupTable(file=ps_file) my_ps = galsim.PowerSpectrum(my_tab, units=galsim.radians) g1, g2 = my_ps.buildGrid(grid_spacing=grid_spacing, ngrid=array_size, units=galsim.degrees, rng=galsim.BaseDeviate(rand_seed)) # Then initialize the PSE object. my_pse = galsim.pse.PowerSpectrumEstimator(N=array_size, sky_size_deg=array_size * grid_spacing, nbin=n_ell) # Estimate the power spectrum using the PSE, without weighting. ell, P_e1, P_b1, P_eb1 = my_pse.estimate(g1, g2) # To check: P_E is right (to within the desired tolerance); P_B and P_EB are <1% of P_E. P_e_theory = np.zeros_like(ell) for ind in range(len(ell)): P_e_theory[ind] = my_tab(ell[ind]) # Note: we don't check the first element because at low ell the tests can fail more # spectacularly for reasons that are well understood. np.testing.assert_array_almost_equal( (P_e1[1:] / P_e_theory[1:] - 1.) / (2 * tolerance), 0., decimal=0, err_msg='PSE returned wrong E power') np.testing.assert_array_almost_equal( (P_b1[1:] / P_e_theory[1:]) / (2 * zero_tolerance), 0., decimal=0, err_msg='PSE found B power') np.testing.assert_array_almost_equal( (P_eb1[1:] / P_e_theory[1:]) / (2 * zero_tolerance), 0., decimal=0, err_msg='PSE found EB cross-power') # Also check the case where P_e=P_b. my_ps = galsim.PowerSpectrum(my_tab, my_tab, units=galsim.radians) g1, g2 = my_ps.buildGrid(grid_spacing=grid_spacing, ngrid=array_size, units=galsim.degrees, rng=galsim.BaseDeviate(rand_seed)) ell, P_e2, P_b2, P_eb2 = my_pse.estimate(g1, g2) np.testing.assert_array_almost_equal( (P_e2[1:] / P_e_theory[1:] - 1.) / (2 * tolerance), 0., decimal=0, err_msg='PSE returned wrong E power') np.testing.assert_array_almost_equal( (P_b2[1:] / P_e_theory[1:] - 1.) / (2 * tolerance), 0., decimal=0, err_msg='PSE returned wrong B power') np.testing.assert_array_almost_equal( (P_eb2[1:] / P_e_theory[1:]) / (2 * zero_tolerance), 0., decimal=0, err_msg='PSE found EB cross-power') # And check the case where P_b is nonzero and P_e is zero. my_ps = galsim.PowerSpectrum(e_power_function=None, b_power_function=my_tab, units=galsim.radians) g1, g2 = my_ps.buildGrid(grid_spacing=grid_spacing, ngrid=array_size, units=galsim.degrees, rng=galsim.BaseDeviate(rand_seed)) ell, P_e3, P_b3, P_eb3 = my_pse.estimate(g1, g2) np.testing.assert_array_almost_equal( (P_e3[1:] / P_e_theory[1:]) / (2 * zero_tolerance), 0., decimal=0, err_msg='PSE found E power when it should be zero') np.testing.assert_array_almost_equal( (P_b3[1:] / P_e_theory[1:] - 1.) / (2 * tolerance), 0., decimal=0, err_msg='PSE returned wrong B power') np.testing.assert_array_almost_equal( (P_eb3[1:] / P_e_theory[1:]) / (2 * zero_tolerance), 0., decimal=0, err_msg='PSE found EB cross-power') t2 = time.time() print 'time for %s = %.2f' % (funcname(), t2 - t1)
def test_tabulated(): """Test using a LookupTable to interpolate a P(k) that is known at certain k""" import time t1 = time.time() # make PowerSpectrum with some obvious analytic form, P(k)=k^2 ps_analytic = galsim.PowerSpectrum(pk2) # now tabulate that analytic form at a range of k k_arr = 0.01*np.arange(10000.)+0.01 p_arr = k_arr**(2.) # make a LookupTable to initialize another PowerSpectrum tab = galsim.LookupTable(k_arr, p_arr) ps_tab = galsim.PowerSpectrum(tab) # draw shears on a grid from both PowerSpectrum objects, with same random seed seed = 12345 g1_analytic, g2_analytic = ps_analytic.buildGrid(grid_spacing = 1., ngrid = 10, rng = galsim.BaseDeviate(seed)) g1_tab, g2_tab = ps_tab.buildGrid(grid_spacing = 1., ngrid = 10, rng = galsim.BaseDeviate(seed)) # make sure that shears that are drawn are essentially identical np.testing.assert_almost_equal(g1_analytic, g1_tab, 6, err_msg = "g1 of shear field from tabulated P(k) differs from expectation!") np.testing.assert_almost_equal(g2_analytic, g2_tab, 6, err_msg = "g2 of shear field from tabulated P(k) differs from expectation!") # now check that we get the same answer if we use file readin: write k and P(k) to a file then # initialize LookupTable from that file data_all = (k_arr, p_arr) data = np.column_stack(data_all) filename = 'lensing_reference_data/tmp.txt' np.savetxt(filename, data) tab2 = galsim.LookupTable(file = filename) ps_tab2 = galsim.PowerSpectrum(tab2) g1_tab2, g2_tab2 = ps_tab2.buildGrid(grid_spacing = 1., ngrid = 10, rng = galsim.BaseDeviate(seed)) np.testing.assert_almost_equal(g1_analytic, g1_tab2, 6, err_msg = "g1 from file-based tabulated P(k) differs from expectation!") np.testing.assert_almost_equal(g2_analytic, g2_tab2, 6, err_msg = "g2 from file-based tabulated P(k) differs from expectation!") # check that we get the same answer whether we use interpolation in log for k, P, or both tab = galsim.LookupTable(k_arr, p_arr, x_log = True) ps_tab = galsim.PowerSpectrum(tab) g1_tab, g2_tab = ps_tab.buildGrid(grid_spacing = 1., ngrid = 10, rng = galsim.BaseDeviate(seed)) np.testing.assert_almost_equal(g1_analytic, g1_tab, 6, err_msg = "g1 of shear field from tabulated P(k) with x_log differs from expectation!") np.testing.assert_almost_equal(g2_analytic, g2_tab, 6, err_msg = "g2 of shear field from tabulated P(k) with x_log differs from expectation!") tab = galsim.LookupTable(k_arr, p_arr, f_log = True) ps_tab = galsim.PowerSpectrum(tab) g1_tab, g2_tab = ps_tab.buildGrid(grid_spacing = 1., ngrid = 10, rng = galsim.BaseDeviate(seed)) np.testing.assert_almost_equal(g1_analytic, g1_tab, 6, err_msg = "g1 of shear field from tabulated P(k) with f_log differs from expectation!") np.testing.assert_almost_equal(g2_analytic, g2_tab, 6, err_msg = "g2 of shear field from tabulated P(k) with f_log differs from expectation!") tab = galsim.LookupTable(k_arr, p_arr, x_log = True, f_log = True) ps_tab = galsim.PowerSpectrum(tab) g1_tab, g2_tab = ps_tab.buildGrid(grid_spacing = 1., ngrid = 10, rng = galsim.BaseDeviate(seed)) np.testing.assert_almost_equal(g1_analytic, g1_tab, 6, err_msg="g1 of shear field from tabulated P(k) with x_log, f_log differs from expectation!") np.testing.assert_almost_equal(g2_analytic, g2_tab, 6, err_msg="g2 of shear field from tabulated P(k) with x_log, f_log differs from expectation!") # check for appropriate response to inputs when making/using LookupTable try: ## mistaken interpolant choice np.testing.assert_raises(ValueError, galsim.LookupTable, k_arr, p_arr, interpolant='splin') ## k, P arrays not the same size np.testing.assert_raises(ValueError, galsim.LookupTable, 0.01*np.arange(100.), p_arr) ## arrays too small np.testing.assert_raises(RuntimeError, galsim.LookupTable, (1.,2.), (1., 2.)) ## try to make shears, but grid includes k values that were not part of the originally ## tabulated P(k) (for this test we make a stupidly limited k grid just to ensure that an ## exception should be raised) t = galsim.LookupTable((0.99,1.,1.01),(0.99,1.,1.01)) ps = galsim.PowerSpectrum(t) np.testing.assert_raises(ValueError, ps.buildGrid, grid_spacing=1., ngrid=100) ## try to interpolate in log, but with zero values included np.testing.assert_raises(ValueError, galsim.LookupTable, (0.,1.,2.), (0.,1.,2.), x_log=True) np.testing.assert_raises(ValueError, galsim.LookupTable, (0.,1.,2.), (0.,1.,2.), f_log=True) np.testing.assert_raises(ValueError, galsim.LookupTable, (0.,1.,2.), (0.,1.,2.), x_log=True, f_log=True) except ImportError: pass # check that when calling LookupTable, the outputs have the same form as inputs tab = galsim.LookupTable(k_arr, p_arr) k = 0.5 assert type(tab(k)) == float k = (0.5, 1.5) result = tab(k) assert type(result) == tuple and len(result) == len(k) k = list(k) result = tab(k) assert type(result) == list and len(result) == len(k) k = np.array(k) result = tab(k) assert type(result) == np.ndarray and len(result) == len(k) k = 0.01+np.zeros((2,2)) result = tab(k) assert type(result) == np.ndarray and result.shape == k.shape # check for expected behavior with log interpolation k = (1., 2., 3.) p = (1., 4., 9.) t = galsim.LookupTable(k, p, interpolant = 'linear') ## a linear interpolant should fail here because P(k) is a power-law, so make sure we get the ## expected result with linear interpolation np.testing.assert_almost_equal(t(2.5), 13./2., decimal = 6, err_msg = 'Unexpected result for linear interpolation of power-law') ## but a linear interpolant works if you work in log space, so check against real result t = galsim.LookupTable(k, p, interpolant = 'linear', x_log = True, f_log = True) np.testing.assert_almost_equal(t(2.5), 2.5**2, decimal = 6, err_msg = 'Unexpected result for linear interpolation of power-law in log space') t2 = time.time() print 'time for %s = %.2f'%(funcname(),t2-t1)
def generateCatalog(self, rng, catalog, parameters, offsets, subfield_index): """For a galaxy catalog with positions included, determine the lensing shear and magnification to assign to each galaxy in the catalog.""" # We need a cache for a grid of shear values covering the entire field, i.e., including all # possible positions in all subfields (modulo sub-pixel offsets from the subfield grid - # we're not trying to take those into account). If there is nothing in the cache for this # field, then make a new grid and save it in the cache. # ps_tab = parameters["ps_tab"] ps_nuisance = parameters["ps_nuisance"] if ps_tab != self.cached_ps_tab: # If nothing is cached for this power spectrum, then first we have to define the power # spectrum in a way that the galsim lensing engine can use it. # Begin by identifying and reading in the proper files for the cosmological part of the # power spectrum. file_index = np.floor(ps_tab) residual = ps_tab - file_index import os infile1 = os.path.join(self.ps_dir , self.infile_pref + self.zmed_str[int(file_index)]+'.out') data1 = np.loadtxt(infile1).transpose() ell = data1[0] p1 = data1[1] infile2 = os.path.join(self.ps_dir , self.infile_pref + self.zmed_str[int(file_index)+1]+'.out') data2 = np.loadtxt(infile2).transpose() p2 = data2[1] # Now make a geometric mean to get the cosmological power spectrum. p_cos = (p1**(1.-residual))*(p2**residual) p_cos *= self.mult_factor # Construct the shapelets nuisance functions x = np.log10(ell/self.ell_piv) n_ell = len(ell) b_values = np.zeros((self.max_order, n_ell)) for order in range(0, self.max_order): b_values[order,:] = self._bn(order, x, self.beta) nuisance_func = np.zeros(n_ell) for order in range(0, self.max_order): nuisance_func += ps_nuisance[order]*b_values[order,:] p_use = p_cos*(1.0+nuisance_func) # Note: units for ell, p_use are 1/radians and radians^2, respectively. # Now, we have arrays we can use to make a power spectrum object with E-mode power # only. While we are at it, we cache it and its parameters. ps_lookup = galsim.LookupTable(ell, p_use, x_log=True, f_log=True) self.cached_ps = galsim.PowerSpectrum(ps_lookup, units = galsim.radians) self.cached_ps_tab = ps_tab self.cached_ps_nuisance = ps_nuisance # Define the grid on which we want to get shears. # This is a little tricky: we have a setup for subfield locations within the field that # is defined in builder.py function generateSubfieldOffsets(). The first subfield is # located at the origin, and to represent it alone, we would need a constants.nrows x # constants.ncols grid of shears. But since we subsample by a parameter given as # constants.subfield_grid_subsampling, each grid dimension must be larger by that # amount. if constants.nrows != constants.ncols: raise NotImplementedError("Currently variable shear grids require nrows=ncols") n_grid = constants.subfield_grid_subsampling * constants.nrows grid_spacing = constants.image_size_deg / n_grid # Run buildGrid() to get the shears and convergences on this grid. However, we also # want to effectively change the value of k_min that is used for the calculation, to get # a reasonable shear correlation function on large scales without excessive truncation. # We also define a grid center such that the position of the first pixel is (0,0). grid_center = 0.5 * (constants.image_size_deg - grid_spacing) self.cached_ps.buildGrid(grid_spacing = grid_spacing, ngrid = n_grid, units = galsim.degrees, rng = rng, center = (grid_center, grid_center), kmin_factor=3) # Now that our cached PS has a grid of shears / convergences, we can use getLensing() to # get the quantities we need for a lensing measurement at any position, so this part of # the calculation is done. # Now get the shears/convergences for each galaxy position in the # catalog. This is fastest if done all at once, with one call to getLensing(). And this is # actually slightly tricky, because we have to take into account: # (1) The position of the galaxy within the subfield. # (2) The offset of the subfield with respect to the field. # And make sure we've gotten the units right for both of these. We are ignoring centroid # shifts of order 1 pixel (max 0.2" for ground data) which can occur within an image. # # We can define object indices in x, y directions - i.e., make indices that range # from 0 to constants.nrows-1. xsize = constants.xsize[self.obs_type][self.multiepoch] ysize = constants.ysize[self.obs_type][self.multiepoch] x_ind = (catalog["x"]+1+0.5*xsize)/xsize-1 y_ind = (catalog["y"]+1+0.5*ysize)/ysize-1 # Turn this into (x, y) positions within the subfield, in degrees. x_pos = x_ind * constants.image_size_deg / constants.nrows y_pos = y_ind * constants.image_size_deg / constants.ncols # But now we have to add the subfield offset. These are calculated as a fraction of the # separation between galaxies, so we have to convert to degrees. x_pos += offsets[0] * constants.image_size_deg / constants.nrows y_pos += offsets[1] * constants.image_size_deg / constants.ncols catalog["g1"], catalog["g2"], catalog["mu"] = \ self.cached_ps.getLensing(pos=(x_pos, y_pos), units=galsim.degrees) # Previous numbers were in degrees. But now we need to save some numbers for ID generation, # which have to be ints. So we will save them in units of subfield grid spacing, i.e., # within a given subfield, galaxies are spaced by constants.subfield_grid_subsampling. # Right now x_ind, y_ind are integers (spaced by 1) and offsets[0] and offsets[1] span the # range (0, 1/constants.subfield_grid_subsampling), so the line below has # constants.subfield_grid_subsampling multiplying both. catalog["x_field_pos"] = np.round( constants.subfield_grid_subsampling * (offsets[0] + x_ind)).astype(int) catalog["y_field_pos"] = np.round( constants.subfield_grid_subsampling * (offsets[1] + y_ind)).astype(int) for record in catalog: record["ID"] = 1e6*subfield_index + 1e3*record["x_field_pos"] + record["y_field_pos"]
n_realization = 1000 pkfile = 'ps.wmap7lcdm.2000.append0.dat' n_ell = 15 grid_nx = 50 theta = 10. # degrees dtheta = theta/grid_nx # degrees outfile = 'output/ps.results.input_pe.pse.dat' ellvals = np.zeros(n_ell) p_e = np.zeros((n_ell, n_realization)) p_b = np.zeros((n_ell, n_realization)) p_eb = np.zeros((n_ell, n_realization)) tab = galsim.LookupTable(file=pkfile, interpolant='linear', x_log=True, f_log=True) test_ps_e=galsim.PowerSpectrum(e_power_function = tab, units='radians') test_ps_b=galsim.PowerSpectrum(b_power_function = tab, units='radians') test_ps_eb=galsim.PowerSpectrum(e_power_function = tab, b_power_function = tab, units='radians') my_pse = pse.PowerSpectrumEstimator(grid_nx, theta, n_ell) for ireal in range(n_realization): print "Iteration ",ireal print "Getting shears on a grid" g1, g2 = test_ps_e.buildGrid(grid_spacing=dtheta, ngrid=grid_nx, units=galsim.degrees) this_ell, this_pe, this_pb, this_peb, this_theory = my_pse.estimate(g1, g2, theory_func=tab) ellvals = this_ell p_e[:,ireal] = this_pe p_b[:,ireal] = this_pb
dudx = np.cos(pos_th.rad()) * pixel_scale dudy = -np.sin(pos_th.rad()) * pixel_scale dvdx = np.sin(pos_th.rad()) * pixel_scale dvdy = np.cos(pos_th.rad()) * pixel_scale affine = galsim.AffineTransform(dudx, dudy, dvdx, dvdy, origin=ps_image.trueCenter()) sky_center = galsim.CelestialCoord(ra=ra_cen_ps * galsim.degrees, dec=dec_cen_ps * galsim.degrees) wcs = galsim.TanWCS(affine, sky_center, units=galsim.arcsec) ps_image.wcs = wcs rng_ps = galsim.BaseDeviate(ps_seed) ps = galsim.PowerSpectrum(ps_file, units=galsim.radians) ps.buildGrid( grid_spacing=grid_spacing, ngrid=int(1 + np.ceil(ps_image_size * pixel_scale / grid_spacing)), rng=rng_ps.duplicate()) elif shear_method == 'constant': reduced_shear = np.sqrt(reduced_g1**2 + reduced_g2**2) logger.info( " Background shear field is assumed to be constant |g|=%.4f" % reduced_shear) g1[:ngal], g2[:ngal] = reduced_g1, reduced_g2 #gt[:ngal] = reduced_shear #beta[:ngal] = imk.theta(ngal,seed=beta_seed) else: # shear_method=="extra": logger.info(" Background shear is from extra catalog %s." % shear_catn.split("/")[-1])
def getAtmosPSFGrid(k, Pk, ngrid=20, dtheta_arcsec=360., kmin_factor=15, subsample=10, rng=None, return_basic=True): """A routine to build an anisotropy and size fluctuation grid for the atmospheric PSF P(k). This routine takes NumPy arrays of k and P(k) values to be used for the E and B power for the atmospheric PSF (same P(k) for E and B), and returns grids of atmospheric e1, e2, and fractional fluctuations in size. Note that the input power spectra are for the ellipticity (distortion) fluctuations. The lensing engine works in terms of shear, and for nearly round objects, shear~distortion/2. Since we gave the lensing engine the PS of distortion fluctuations, the return values can be used directly for PSF ellipticity (distortion), but the kappa values are too high by a factor of 2. The routine requires k and Pk, and has a number of optional parameters that default to what will be used for GREAT3: ngrid Number of grid points for galaxies in one dimension (default 20, for the 2x2 degree subfields used for PSF estimation). dtheta_arcsec Spacing between grid points for the galaxies in arcsec (default 360, i.e., 0.1 degree). kmin_factor Factor by which to spatially extend the grids to get a smaller kmin value, growing them larger by this factor in each dimension so as to properly represent the large-scale shear correlations (default 20). subsample Factor by which to subsample the grid, so as to get several offset realizations of the same atmosphere (default 10). [Note, this is like kmax_factor for the lensing engine work on GalSim issue #377, but there, the idea is to represent smaller kmax without actually getting back a more densely packed grid. Here, we actually do want a more densely packed grid, so I'm calling it subsample to suggest the explicit subsampling of the grid. rng RNG to use for the generation of these fields. return_basic Return the basic grid that is not enlarged by kmin_factor (if True), or actually return the huge grid (if False). [Default = True] The results are returned as three NumPy arrays for the PSF e1, PSF e2, and fractional change in size of the PSF. """ # Set up the PowerSpectrum object. tab_pk = galsim.LookupTable(k, Pk, x_log=True, f_log=True) ps = galsim.PowerSpectrum(tab_pk, tab_pk, units=galsim.arcsec) # Use buildGrid() to get the grid. Note that with the code in GalSim issue #377, this code could # be simplified. It automatically takes care of the `kmin_factor` expansion of the grid. This # would also lead to simplification of the code below where the return values are selected based # on return_basic. e1, e2, kappa = ps.buildGrid(grid_spacing = dtheta_arcsec/subsample, ngrid = ngrid*kmin_factor*subsample, get_convergence = True, rng = rng) # Redefine the kappa's kappa /= 2. # Take subset of the overly huge grid. Since the grid is periodic, can just take one corner, don't # have to try to be in the middle. if return_basic is False: # make grid positions ntot = ngrid*kmin_factor*subsample grid_spacing = dtheta_arcsec/subsample/60. min = (-ntot/2 + 0.5) * grid_spacing max = (ntot/2 - 0.5) * grid_spacing x, y = np.meshgrid(np.arange(min,max+grid_spacing,grid_spacing), np.arange(min,max+grid_spacing,grid_spacing)) return e1, e2, kappa, x, y else: n_use = ngrid*subsample grid_spacing = dtheta_arcsec/subsample/60. min = (-n_use/2 + 0.5) * grid_spacing max = (n_use/2 - 0.5) * grid_spacing x, y = np.meshgrid(np.arange(min,max+grid_spacing,grid_spacing), np.arange(min,max+grid_spacing,grid_spacing)) return e1[0:n_use, 0:n_use], e2[0:n_use, 0:n_use], kappa[0:n_use, 0:n_use], x-min, y-min
def test_shear_variance(): """Test that shears from several toy power spectra have the expected variances.""" import time t1 = time.time() # setup the random number generator to use for these tests rng = galsim.BaseDeviate(512342) # set up grid parameters grid_size = 50. # degrees ngrid = 500 # grid points klim = klim_test # now get derived grid parameters kmin = 2.*np.pi/grid_size/3600. # arcsec^-1 # Make a flat power spectrum for E, B modes with P=1 arcsec^2, truncated above some limiting # value of k. # Given our grid size of 50 degrees [which is silly to do for a flat-sky approximation, but # we're just doing it anyway to beat down the noise], the minimum k we can probe is 2pi/50 # deg^{-1} = 3.49e-5 arcsec^-1. With 500 grid points, the maximum k in one dimension is 250 # times as large, 0.00873 arcsec^-1. The function pk_flat_lim is 0 for k>klim_test=0.00175 which # is a factor of 5 below our maximum k, a factor of ~50 above our minimum k. For k<=0.00175, # pk_flat_lim returns 1. test_ps = galsim.PowerSpectrum(e_power_function=pk_flat_lim, b_power_function=pk_flat_lim) # get shears on 500x500 grid with spacing 0.1 degree g1, g2 = test_ps.buildGrid(grid_spacing=grid_size/ngrid, ngrid=ngrid, rng=rng, units=galsim.degrees) assert g1.shape == (ngrid, ngrid) assert g2.shape == (ngrid, ngrid) # Now we should compare the variance with the predictions. We use # ../devel/modules/lensing_engine.pdf section 5.3 to get # Var(g1) + Var(g2) = (1/pi^2) [(pi klim^2 / 4) - kmin^2] # Here the 1 on top of the pi^2 is actually P0 which has units of arcsec^2. # A final point for this test is that the result should be twice as large than that prediction # since we have both E and B power. And we know from before that due to various effects, the # results should actually be ~1.5% too low. predicted_variance = (1./np.pi**2)*(0.25*np.pi*(klim**2) - kmin**2) predicted_variance *= 2 var1 = np.var(g1) var2 = np.var(g2) print 'predicted variance = ',predicted_variance print 'actual variance = ',var1+var2 print 'fractional diff = ',((var1+var2)/predicted_variance-1) assert np.abs((var1+var2) - predicted_variance) < 0.015 * predicted_variance, \ "Incorrect shear variance from flat power spectrum!" # check: are g1, g2 uncorrelated with each other? top= np.sum((g1-np.mean(g1))*(g2-np.mean(g2))) bottom1 = np.sum((g1-np.mean(g1))**2) bottom2 = np.sum((g2-np.mean(g2))**2) corr = top / np.sqrt(bottom1*bottom2) np.testing.assert_almost_equal( corr, 0., decimal=1, err_msg="Shear components should be uncorrelated with each other! (flat power spectrum)") # Now do the same test as previously, but with E-mode power only. test_ps = galsim.PowerSpectrum(e_power_function=pk_flat_lim) g1, g2 = test_ps.buildGrid(grid_spacing=grid_size/ngrid, ngrid=ngrid, rng=rng, units=galsim.degrees) assert g1.shape == (ngrid, ngrid) assert g2.shape == (ngrid, ngrid) predicted_variance = (1./np.pi**2)*(0.25*np.pi*(klim**2) - kmin**2) var1 = np.var(g1) var2 = np.var(g2) print 'predicted variance = ',predicted_variance print 'actual variance = ',var1+var2 print 'fractional diff = ',((var1+var2)/predicted_variance-1) assert np.abs((var1+var2) - predicted_variance) < 0.015 * predicted_variance, \ "Incorrect shear variance from flat E-mode power spectrum!" # check: are g1, g2 uncorrelated with each other? top= np.sum((g1-np.mean(g1))*(g2-np.mean(g2))) bottom1 = np.sum((g1-np.mean(g1))**2) bottom2 = np.sum((g2-np.mean(g2))**2) corr = top / np.sqrt(bottom1*bottom2) np.testing.assert_almost_equal( corr, 0., decimal=1, err_msg="Shear components should be uncorrelated with each other! (flat E-mode power spec.)") # check for proper scaling with grid spacing, for fixed number of grid points grid_size = 25. # degrees ngrid = 500 # grid points klim = klim_test kmin = 2.*np.pi/grid_size/3600. # arcsec^-1 test_ps = galsim.PowerSpectrum(e_power_function=pk_flat_lim, b_power_function=pk_flat_lim) g1, g2 = test_ps.buildGrid(grid_spacing=grid_size/ngrid, ngrid=ngrid, rng=rng, units=galsim.degrees) assert g1.shape == (ngrid, ngrid) assert g2.shape == (ngrid, ngrid) predicted_variance = (1./np.pi**2)*(0.25*np.pi*(klim**2) - kmin**2) predicted_variance *= 2 var1 = np.var(g1) var2 = np.var(g2) print 'predicted variance = ',predicted_variance print 'actual variance = ',var1+var2 print 'fractional diff = ',((var1+var2)/predicted_variance-1) assert np.abs((var1+var2) - predicted_variance) < 0.015 * predicted_variance, \ "Incorrect shear variance from flat power spectrum with smaller grid_size" # check for proper scaling with number of grid points, for fixed grid spacing grid_size = 25. # degrees ngrid = 250 # grid points klim = klim_test kmin = 2.*np.pi/grid_size/3600. # arcsec^-1 test_ps = galsim.PowerSpectrum(e_power_function=pk_flat_lim, b_power_function=pk_flat_lim) g1, g2 = test_ps.buildGrid(grid_spacing=grid_size/ngrid, ngrid=ngrid, rng=rng, units=galsim.degrees) assert g1.shape == (ngrid, ngrid) assert g2.shape == (ngrid, ngrid) predicted_variance = (1./np.pi**2)*(0.25*np.pi*(klim**2) - kmin**2) predicted_variance *= 2 var1 = np.var(g1) var2 = np.var(g2) print 'predicted variance = ',predicted_variance print 'actual variance = ',var1+var2 print 'fractional diff = ',((var1+var2)/predicted_variance-1) assert np.abs((var1+var2) - predicted_variance) < 0.015 * predicted_variance, \ "Incorrect shear variance from flat power spectrum with smaller ngrid" # Test one other theoretical PS: the Gaussian P(k). # We define it as P(k) = exp(-s^2 k^2 / 2). # First set up the grid. grid_size = 50. # degrees ngrid = 500 # grid points kmin = 2.*np.pi/grid_size/3600. kmax = np.pi/(grid_size/ngrid)/3600. # For explanation of these two variables, see below, the comment starting "Note: the next..." # These numbers are, however, hard-coded up here with the grid parameters because if the grid is # changed, the erfmax and erfmin must change. erfmax = 0.9875806693484477 erfmin = 0.007978712629263206 # Now choose s such that s*kmax=2.5, i.e., very little power at kmax. s = 2.5/kmax test_ps = galsim.PowerSpectrum(lambda k : np.exp(-0.5*((s*k)**2))) g1, g2 = test_ps.buildGrid(grid_spacing = grid_size/ngrid, ngrid=ngrid, rng=rng, units=galsim.degrees) assert g1.shape == (ngrid, ngrid) assert g2.shape == (ngrid, ngrid) # For this case, the prediction for the variance is: # Var(g1) + Var(g2) = [1/(2 pi s^2)] * ( (Erf(s*kmax/sqrt(2)))^2 - (Erf(s*kmin/sqrt(2)))^2 ) # Note: the next two lines of code are commented out because math.erf is not available in python # v2.6, with which we must be compatible. So instead the values of erf are hard-coded (above) # based on the calculations from a machine that has python v2.7. The implications here are that # if one changes the grid parameters for this test in a way that also changes the values of # these Erf[...] calculations, then the hard-coded erfmax and erfmin must be changed. # erfmax = math.erf(s*kmax/math.sqrt(2.)) # erfmin = math.erf(s*kmin/math.sqrt(2.)) var1 = np.var(g1) var2 = np.var(g2) predicted_variance = (erfmax**2 - erfmin**2) / (2.*np.pi*(s**2)) print 'predicted variance = ',predicted_variance print 'actual variance = ',var1+var2 print 'fractional diff = ',((var1+var2)/predicted_variance-1) assert np.abs((var1+var2) - predicted_variance) < 0.015 * predicted_variance, \ "Incorrect shear variance from Gaussian power spectrum" # check for proper scaling with grid spacing, for fixed number of grid points grid_size = 25. # degrees ngrid = 500 # grid points kmin = 2.*np.pi/grid_size/3600. kmax = np.pi/(grid_size/ngrid)/3600. s = 2.5/kmax # Note that because of how s, kmin, and kmax change, the Erf[...] quantities do not change. So # we don't have to reset the values here. test_ps = galsim.PowerSpectrum(lambda k : np.exp(-0.5*((s*k)**2))) g1, g2 = test_ps.buildGrid(grid_spacing = grid_size/ngrid, ngrid=ngrid, rng=rng, units=galsim.degrees) assert g1.shape == (ngrid, ngrid) assert g2.shape == (ngrid, ngrid) var1 = np.var(g1) var2 = np.var(g2) predicted_variance = (erfmax**2 - erfmin**2) / (2.*np.pi*(s**2)) print 'predicted variance = ',predicted_variance print 'actual variance = ',var1+var2 print 'fractional diff = ',((var1+var2)/predicted_variance-1) assert np.abs((var1+var2) - predicted_variance) < 0.015 * predicted_variance, \ "Incorrect shear variance from Gaussian power spectrum with smaller grid_size" # check for proper scaling with number of grid points, for fixed grid spacing grid_size = 25. # degrees ngrid = 250 # grid points kmin = 2.*np.pi/grid_size/3600. kmax = np.pi/(grid_size/ngrid)/3600. # Here one of the Erf[...] values does change. erfmin = 0.01595662743380396 s = 2.5/kmax test_ps = galsim.PowerSpectrum(lambda k : np.exp(-0.5*((s*k)**2))) g1, g2 = test_ps.buildGrid(grid_spacing = grid_size/ngrid, ngrid=ngrid, rng=rng, units=galsim.degrees) assert g1.shape == (ngrid, ngrid) assert g2.shape == (ngrid, ngrid) var1 = np.var(g1) var2 = np.var(g2) predicted_variance = (erfmax**2 - erfmin**2) / (2.*np.pi*(s**2)) print 'predicted variance = ',predicted_variance print 'actual variance = ',var1+var2 print 'fractional diff = ',((var1+var2)/predicted_variance-1) assert np.abs((var1+var2) - predicted_variance) < 0.015 * predicted_variance, \ "Incorrect shear variance from Gaussian power spectrum with smaller ngrid" # change grid spacing implicitly via kmax_factor # This and the next test can be made at higher precision (0.5% rather than 1.5%), since the # grids actually used to make the shears have more points, so they are more accurate. grid_size = 50. # degrees ngrid = 500 # grid points kmax_factor = 2 kmin = 2.*np.pi/grid_size/3600. kmax = np.pi/(grid_size/ngrid)/3600.*kmax_factor # Back to the original erfmin value. erfmin = 0.007978712629263206 s = 2.5/kmax test_ps = galsim.PowerSpectrum(lambda k : np.exp(-0.5*((s*k)**2))) g1, g2 = test_ps.buildGrid(grid_spacing = grid_size/ngrid, ngrid=ngrid, rng=rng, units=galsim.degrees, kmax_factor=kmax_factor) assert g1.shape == (ngrid, ngrid) assert g2.shape == (ngrid, ngrid) var1 = np.var(g1) var2 = np.var(g2) predicted_variance = (erfmax**2 - erfmin**2) / (2.*np.pi*(s**2)) print 'predicted variance = ',predicted_variance print 'actual variance = ',var1+var2 print 'fractional diff = ',((var1+var2)/predicted_variance-1) assert np.abs((var1+var2) - predicted_variance) < 0.005 * predicted_variance, \ "Incorrect shear variance from Gaussian power spectrum with kmax_factor=2" # change ngrid implicitly with kmin_factor grid_size = 50. # degrees ngrid = 500 # grid points kmin_factor = 2 kmin = 2.*np.pi/grid_size/3600./kmin_factor kmax = np.pi/(grid_size/ngrid)/3600. s = 2.5/kmax # This time, erfmin is smaller. erfmin = 0.003989406181481644 test_ps = galsim.PowerSpectrum(lambda k : np.exp(-0.5*((s*k)**2))) g1, g2 = test_ps.buildGrid(grid_spacing = grid_size/ngrid, ngrid=ngrid, rng=rng, units=galsim.degrees, kmin_factor=kmin_factor) assert g1.shape == (ngrid, ngrid) assert g2.shape == (ngrid, ngrid) var1 = np.var(g1) var2 = np.var(g2) predicted_variance = (erfmax**2 - erfmin**2) / (2.*np.pi*(s**2)) print 'predicted variance = ',predicted_variance print 'actual variance = ',var1+var2 print 'fractional diff = ',((var1+var2)/predicted_variance-1) assert np.abs((var1+var2) - predicted_variance) < 0.005 * predicted_variance, \ "Incorrect shear variance from Gaussian power spectrum with kmin_factor=2" t2 = time.time() print 'time for %s = %.2f'%(funcname(),t2-t1)
do_shear = True # Also we'll make an option to just use some of the grid, to save time. use_subgrid = True if do_shear: for t0 in theta0: print "Getting shears using GalSim for theta0=",t0 strval = str(int(round(t0))) infile = 'pk_math/Pk'+strval+'.dat' pk_dat = np.loadtxt(infile).transpose() k = pk_dat[0] pk = 1.e-4*2.*np.pi*pk_dat[1] # put in the normalization here tab_pk = galsim.LookupTable(k, 0.5*pk, x_log=True, f_log=True) # Take the outputs and run corr2 to check that the outputs correspond to inputs. Use 0.5*P # but give that to both P_E and P_B. ps = galsim.PowerSpectrum(tab_pk, tab_pk, units=galsim.arcsec) # Note, typically the buildGrid() method returns shear, not distortion e. For nearly round # things, e~2*shear. But for the atmospheric PSF stuff, I gave the code amplitudes # corresponding to some ellipticity variance, so the results should correspond to e1/e2 in # amplitude as well. The alternative approach would have been to use shear variances # (factor of 4 lower) and then to take the e that come out and multiply by 2. This seemed # silly so I didn't do it. However, it does mean that the kappa fluctuations are too high # by a factor of 4, so I must reduce them. e1, e2, kappa = ps.buildGrid(grid_spacing=dtheta_arcsec, ngrid=ngrid, get_convergence=True) kappa /= 4. grid_range = (dtheta_arcsec) * np.arange(ngrid) x, y = np.meshgrid(grid_range, grid_range) if not use_subgrid:
def main(argv): """ Make images using variable PSF and shear: - The main image is 10 x 10 postage stamps. - Each postage stamp is 48 x 48 pixels. - The second HDU has the corresponding PSF image. - Applied shear is from a power spectrum P(k) ~ k^1.8. - Galaxies are real galaxies oriented in a ring test of 20 each. - The PSF is Gaussian with FWHM, ellipticity and position angle functions of (x,y) - Noise is Poisson using a nominal sky value of 1.e6. """ logging.basicConfig(format="%(message)s", level=logging.INFO, stream=sys.stdout) logger = logging.getLogger("demo10") # Define some parameters we'll use below. # Normally these would be read in from some parameter file. n_tiles = 10 # number of tiles in each direction. stamp_size = 48 # pixels pixel_scale = 0.44 # arcsec / pixel sky_level = 1.e6 # ADU / arcsec^2 # The random seed is used for both the power spectrum realization and the random properties # of the galaxies. random_seed = 3339201 # Make output directory if not already present. if not os.path.isdir('output'): os.mkdir('output') file_name = os.path.join('output', 'power_spectrum.fits') # These will be created for each object below. The values we'll use will be functions # of (x,y) relative to the center of the image. (r = sqrt(x^2+y^2)) # psf_fwhm = 0.9 + 0.5 * (r/100)^2 -- arcsec # psf_e = 0.4 * (r/100)^1.5 -- large value at the edge, so visible by eye. # psf_beta = atan2(y/x) + pi/2 -- tangential pattern gal_dilation = 3 # Make the galaxies a bit larger than their original size. gal_signal_to_noise = 100 # Pretty high. psf_signal_to_noise = 1000 # Even higher. logger.info('Starting demo script 10') # Read in galaxy catalog cat_file_name = 'real_galaxy_catalog_example.fits' dir = 'data' real_galaxy_catalog = galsim.RealGalaxyCatalog(cat_file_name, dir=dir) logger.info('Read in %d real galaxies from catalog', real_galaxy_catalog.nobjects) # List of IDs to use. We select 5 particularly irregular galaxies for this demo. # Then we'll choose randomly from this list. id_list = [106416, 106731, 108402, 116045, 116448] # Make the 5 galaxies we're going to use here rather than remake them each time. # This means the Fourier transforms of the real galaxy images don't need to be recalculated # each time, so it's a bit more efficient. gal_list = [ galsim.RealGalaxy(real_galaxy_catalog, id=id) for id in id_list ] # Make the galaxies a bit larger than their original observed size. gal_list = [gal.dilate(gal_dilation) for gal in gal_list] # Setup the PowerSpectrum object we'll be using: ps = galsim.PowerSpectrum(lambda k: k**1.8) # The argument here is "e_power_function" which defines the E-mode power to use. # There is also a b_power_function if you want to include any B-mode power: # ps = galsim.PowerSpectrum(e_power_function, b_power_function) # You may even omit the e_power_function argument and have a pure B-mode power spectrum. # ps = galsim.PowerSpectrum(b_power_function = b_power_function) # All the random number generator classes derive from BaseDeviate. # When we construct another kind of deviate class from any other # kind of deviate class, the two share the same underlying random number # generator. Sometimes it can be clearer to just construct a BaseDeviate # explicitly and then construct anything else you need from that. # Note: A BaseDeviate cannot be used to generate any values. It can # only be used in the constructor for other kinds of deviates. # The seeds for the objects are random_seed..random_seed+nobj-1 (which comes later), # so use the next one. nobj = n_tiles * n_tiles rng = galsim.BaseDeviate(random_seed + nobj) # Setup the images: gal_image = galsim.ImageF(stamp_size * n_tiles, stamp_size * n_tiles) psf_image = galsim.ImageF(stamp_size * n_tiles, stamp_size * n_tiles) # Update the image WCS to use the image center as the origin of the WCS. # The class that acts like a PixelScale except for this offset is called OffsetWCS. im_center = gal_image.bounds.trueCenter() wcs = galsim.OffsetWCS(scale=pixel_scale, origin=im_center) gal_image.wcs = wcs psf_image.wcs = wcs # We will place the tiles in a random order. To do this, we make two lists for the # ix and iy values. Then we apply a random permutation to the lists (in tandem). ix_list = [] iy_list = [] for ix in range(n_tiles): for iy in range(n_tiles): ix_list.append(ix) iy_list.append(iy) # This next function will use the given random number generator, rng, and use it to # randomly permute any number of lists. All lists will have the same random permutation # applied. galsim.random.permute(rng, ix_list, iy_list) # Now have the PowerSpectrum object build a grid of shear values for us to use. # Also, because of some technical details about how the config stuff handles the random # number generator here, we need to duplicate the rng object if we want to have the # two output files match. This means that technically, the same sequence of random numbers # will be used in building the grid as will be used by the other uses of rng (permuting the # postage stamps and adding noise). But since they are used in such completely different # ways, it is hard to imagine how this could lead to any kind of bias in the images. grid_g1, grid_g2 = ps.buildGrid(grid_spacing=stamp_size * pixel_scale, ngrid=n_tiles, rng=rng.duplicate()) # Build each postage stamp: for k in range(nobj): # The usual random number generator using a different seed for each galaxy. rng = galsim.BaseDeviate(random_seed + k) # Determine the bounds for this stamp and its center position. ix = ix_list[k] iy = iy_list[k] b = galsim.BoundsI(ix * stamp_size + 1, (ix + 1) * stamp_size, iy * stamp_size + 1, (iy + 1) * stamp_size) sub_gal_image = gal_image[b] sub_psf_image = psf_image[b] pos = wcs.toWorld(b.trueCenter()) # The image comes out as about 211 arcsec across, so we define our variable # parameters in terms of (r/100 arcsec), so roughly the scale size of the image. r = math.sqrt(pos.x**2 + pos.y**2) / 100 psf_fwhm = 0.9 + 0.5 * r**2 # arcsec psf_e = 0.4 * r**1.5 psf_beta = (math.atan2(pos.y, pos.x) + math.pi / 2) * galsim.radians # Define the PSF profile psf = galsim.Gaussian(fwhm=psf_fwhm) psf = psf.shear(e=psf_e, beta=psf_beta) # Define the galaxy profile: # For this demo, we are doing a ring test where the same galaxy profile is drawn at many # orientations stepped uniformly in angle, making a ring in e1-e2 space. # We're drawing each profile at 20 different orientations and then skipping to the # next galaxy in the list. So theta steps by 1/20 * 360 degrees: theta = k / 20. * 360. * galsim.degrees # The index needs to increment every 20 objects so we use k/20 using integer math. index = k / 20 gal = gal_list[index] # This makes a new copy so we're not changing the object in the gal_list. gal = gal.rotate(theta) # Apply the shear from the power spectrum. We should either turn the gridded shears # grid_g1[iy, ix] and grid_g2[iy, ix] into gridded reduced shears using a utility called # galsim.lensing.theoryToObserved, or use ps.getShear() which by default gets the reduced # shear. ps.getShear() is also more flexible because it can get the shear at positions that # are not on the original grid, as long as they are contained within the bounds of the full # grid. So in this example we'll use ps.getShear(). alt_g1, alt_g2 = ps.getShear(pos) gal = gal.shear(g1=alt_g1, g2=alt_g2) # Apply half-pixel shift in a random direction. shift_r = pixel_scale * 0.5 ud = galsim.UniformDeviate(rng) theta = ud() * 2. * math.pi dx = shift_r * math.cos(theta) dy = shift_r * math.sin(theta) gal = gal.shift(dx, dy) # Make the final image, convolving with the psf final = galsim.Convolve([psf, gal]) # Draw the image final.drawImage(sub_gal_image) # Now add noise to get our desired S/N # See demo5.py for more info about how this works. sky_level_pixel = sky_level * pixel_scale**2 noise = galsim.PoissonNoise(rng, sky_level=sky_level_pixel) sub_gal_image.addNoiseSNR(noise, gal_signal_to_noise) # For the PSF image, we also shift the PSF by the same amount. psf = psf.shift(dx, dy) # Draw the PSF image: # We use real space integration over the pixels to avoid some of the # artifacts that can show up with Fourier convolution. # The level of the artifacts is quite low, but when drawing with # so little noise, they are apparent with ds9's zscale viewing. psf.drawImage(sub_psf_image, method='real_space') # Again, add noise, but at higher S/N this time. sub_psf_image.addNoiseSNR(noise, psf_signal_to_noise) logger.info('Galaxy (%d,%d): position relative to center = %s', ix, iy, str(pos)) logger.info('Done making images of postage stamps') # Now write the images to disk. images = [gal_image, psf_image] galsim.fits.writeMulti(images, file_name) logger.info('Wrote image to %r', file_name)
def test_power_spectrum_with_kappa(): """Test that the convergence map generated by the PowerSpectrum class is consistent with the Kaiser Squires inversion of the corresponding shear field. """ import time t1 = time.time() # Note that in order for this test to pass, we have to control aliasing by smoothing the power # spectrum to go to zero above some maximum k. This is the only way to get agreement at high # precision between the gamma's and kappa's from the lensing engine vs. that from a Kaiser and # Squires inversion. rseed=177774 ngrid=100 dx_grid_arcmin = 6 # First lookup a cosmologically relevant power spectrum (bandlimited version to remove aliasing # and allow high-precision comparison). tab_ps = galsim.LookupTable( file='../examples/data/cosmo-fid.zmed1.00_smoothed.out', interpolant='linear') # Begin with E-mode input power psE = galsim.PowerSpectrum(tab_ps, None, units=galsim.radians) g1E, g2E, k_test = psE.buildGrid( grid_spacing=dx_grid_arcmin, ngrid=ngrid, units=galsim.arcmin, rng=galsim.BaseDeviate(rseed), get_convergence=True) kE_ks, kB_ks = galsim.lensing_ps.kappaKaiserSquires(g1E, g2E) # Test that E-mode kappa matches to some sensible accuracy exact_dp = 15 np.testing.assert_array_almost_equal( k_test, kE_ks, decimal=exact_dp, err_msg="E-mode only PowerSpectrum output kappaE does not match KS inversion to 16 d.p.") # Test that B-mode kappa matches zero to some sensible accuracy np.testing.assert_array_almost_equal( kB_ks, np.zeros_like(kE_ks), decimal=exact_dp, err_msg="E-mode only PowerSpectrum output kappaB from KS does not match zero to 16 d.p.") # Then do B-mode only input power psB = galsim.PowerSpectrum(None, tab_ps, units=galsim.radians) g1B, g2B, k_test = psB.buildGrid( grid_spacing=dx_grid_arcmin, ngrid=ngrid, units=galsim.arcmin, rng=galsim.BaseDeviate(rseed), get_convergence=True) kE_ks, kB_ks = galsim.lensing_ps.kappaKaiserSquires(g1B, g2B) # Test that kappa output by PS code matches zero to some sensible accuracy np.testing.assert_array_almost_equal( k_test, np.zeros_like(k_test), decimal=exact_dp, err_msg="B-mode only PowerSpectrum output kappa does not match zero to 16 d.p.") # Test that E-mode kappa inferred via KS also matches zero to some sensible accuracy np.testing.assert_array_almost_equal( kE_ks, np.zeros_like(kB_ks), decimal=exact_dp, err_msg="B-mode only PowerSpectrum output kappaE from KS does not match zero to 16 d.p.") # Then for luck take B-mode only shears but rotate by 45 degrees before KS, and check # consistency kE_ks_rotated, kB_ks_rotated = galsim.lensing_ps.kappaKaiserSquires(g2B, -g1B) np.testing.assert_array_almost_equal( kE_ks_rotated, kB_ks, decimal=exact_dp, err_msg="KS inverted kappaE from B-mode only PowerSpectrum fails rotation test.") np.testing.assert_array_almost_equal( kB_ks_rotated, np.zeros_like(kB_ks), decimal=exact_dp, err_msg="KS inverted kappaB from B-mode only PowerSpectrum fails rotation test.") # Finally, do E- and B-mode power psB = galsim.PowerSpectrum(tab_ps, tab_ps, units=galsim.radians) g1EB, g2EB, k_test = psB.buildGrid( grid_spacing=dx_grid_arcmin, ngrid=ngrid, units=galsim.arcmin, rng=galsim.BaseDeviate(rseed), get_convergence=True) kE_ks, kB_ks = galsim.lensing_ps.kappaKaiserSquires(g1EB, g2EB) # Test that E-mode kappa matches to some sensible accuracy np.testing.assert_array_almost_equal( k_test, kE_ks, decimal=exact_dp, err_msg="E/B PowerSpectrum output kappa does not match KS inversion to 16 d.p.") # Test rotating the shears by 45 degrees kE_ks_rotated, kB_ks_rotated = galsim.lensing_ps.kappaKaiserSquires(g2EB, -g1EB) np.testing.assert_array_almost_equal( kE_ks_rotated, kB_ks, decimal=exact_dp, err_msg="KS inverted kappaE from E/B PowerSpectrum fails rotation test.") np.testing.assert_array_almost_equal( kB_ks_rotated, -kE_ks, decimal=exact_dp, err_msg="KS inverted kappaB from E/B PowerSpectrum fails rotation test.") t2 = time.time() print 'time for %s = %.2f'%(funcname(),t2-t1)
g1_col='g1', g2_col='g2') # Define the corrfunc object gg = treecorr.GGCorrelation(min_sep=min_sep, max_sep=max_sep, bin_size=0.1, sep_units='degrees') # Actually calculate the correlation function. gg.process(cat) os.remove('temp.fits') return gg # Here's where we actually do stuff. Start by making the PowerSpectrum object, and defining the # grid range. test_ps = galsim.PowerSpectrum(e_power_function=theory_tab, units='radians') grid_range = dtheta * np.arange(grid_nx) x, y = np.meshgrid(grid_range, grid_range) # Now we do the iterations to build the shear grids. for ind in range(n_iter): print 'Building grid %d' % ind g1, g2 = test_ps.buildGrid(grid_spacing=dtheta, ngrid=grid_nx, rng=rng, units='degrees', kmin_factor=kmin_factor, kmax_factor=kmax_factor) print 'Calculating correlations %d' % ind gg = run_treecorr(x, y, g1, g2)
def main(argv): """ Make images using constant PSF and variable shear: - The main image is 0.2 x 0.2 degrees. - Pixel scale is 0.2 arcsec, hence the image is 3600 x 3600 pixels. - Applied shear is from a cosmological power spectrum read in from file. - The PSF is a real one from SDSS, and corresponds to a convolution of atmospheric PSF, optical PSF, and pixel response, which has been sampled at pixel centers. We used a PSF from SDSS in order to have a PSF profile that could correspond to what you see with a real telescope. However, in order that the galaxy resolution not be too poor, we tell GalSim that the pixel scale for that PSF image is 0.2" rather than 0.396". We are simultaneously lying about the intrinsic size of the PSF and about the pixel scale when we do this. - Noise is correlated with the same spatial correlation function as found in HST COSMOS weak lensing science images, with a point (zero distance) variance that we normalize to 1.e4. - Galaxies are real galaxies, each with S/N~100 based on a point variance-only calculation (such as discussed in Leauthaud et al 2007). The true SNR is somewhat lower, due to the presence of correlation in the noise. """ logging.basicConfig(format="%(message)s", level=logging.INFO, stream=sys.stdout) logger = logging.getLogger("demo11") # Define some parameters we'll use below. # Normally these would be read in from some parameter file. stamp_size = 100 # number of pixels in each dimension of galaxy images pixel_scale = 0.2 # arcsec/pixel image_size = 0.2 * galsim.degrees # size of big image in each dimension image_size = int( (image_size / galsim.arcsec) / pixel_scale) # convert to pixels image_size_arcsec = image_size * pixel_scale # size of big image in each dimension (arcsec) noise_variance = 1.e4 # ADU^2 nobj = 288 # number of galaxies in entire field # (This corresponds to 2 galaxies / arcmin^2) grid_spacing = 90.0 # The spacing between the samples for the power spectrum # realization (arcsec) gal_signal_to_noise = 100 # S/N of each galaxy # random_seed is used for both the power spectrum realization and the random properties # of the galaxies. random_seed = 24783923 file_name = os.path.join('output', 'tabulated_power_spectrum.fits.fz') logger.info('Starting demo script 11') # Read in galaxy catalog cat_file_name = 'real_galaxy_catalog_example.fits' # This script is designed to be run from the examples directory so dir is a relative path. # But the '../examples/' part lets bin/demo11 also be run from the bin directory. dir = '../examples/data' real_galaxy_catalog = galsim.RealGalaxyCatalog(cat_file_name, dir=dir) real_galaxy_catalog.preload() logger.info('Read in %d real galaxies from catalog', real_galaxy_catalog.nobjects) # List of IDs to use. We select 5 particularly irregular galaxies for this demo. # Then we'll choose randomly from this list. id_list = [106416, 106731, 108402, 116045, 116448] # Make the 5 galaxies we're going to use here rather than remake them each time. # This means the Fourier transforms of the real galaxy images don't need to be recalculated # each time, so it's a bit more efficient. gal_list = [ galsim.RealGalaxy(real_galaxy_catalog, id=id) for id in id_list ] # Setup the PowerSpectrum object we'll be using: # To do this, we first have to read in the tabulated power spectrum. # We use a tabulated power spectrum from iCosmo (http://icosmo.org), with the following # cosmological parameters and survey design: # H_0 = 70 km/s/Mpc # Omega_m = 0.25 # Omega_Lambda = 0.75 # w_0 = -1.0 # w_a = 0.0 # n_s = 0.96 # sigma_8 = 0.8 # Smith et al. prescription for the non-linear power spectrum. # Eisenstein & Hu transfer function with wiggles. # Default dN/dz with z_med = 1.0 # The file has, as required, just two columns which are k and P(k). However, iCosmo works in # terms of ell and C_ell; ell is inverse radians and C_ell in radians^2. Since GalSim tends to # work in terms of arcsec, we have to tell it that the inputs are radians^-1 so it can convert # to store in terms of arcsec^-1. pk_file = os.path.join('..', 'examples', 'data', 'cosmo-fid.zmed1.00.out') ps = galsim.PowerSpectrum(pk_file, units=galsim.radians) # The argument here is "e_power_function" which defines the E-mode power to use. logger.info('Set up power spectrum from tabulated P(k)') # Now let's read in the PSF. It's a real SDSS PSF, which means pixel scale of 0.396". However, # the typical seeing is 1.2" and we want to simulate better seeing, so we will just tell GalSim # that the pixel scale is 0.2". We have to be careful with SDSS PSF images, as they have an # added 'soft bias' of 1000 which has been removed before creation of this file, so that the sky # level is properly zero. Also, the file is bzipped, to demonstrate the new capability of # reading in a file that has been compressed in various ways (which GalSim can infer from the # filename). We want to read the image directly into an InterpolatedImage GSObject, so we can # manipulate it as needed (here, the only manipulation needed is convolution). We want a PSF # with flux 1, and we can set the pixel scale using a keyword. psf_file = os.path.join('..', 'examples', 'data', 'example_sdss_psf_sky0.fits.bz2') psf = galsim.InterpolatedImage(psf_file, dx=pixel_scale, flux=1.) # We do not include a pixel response function galsim.Pixel here, because the image that was read # in from file already included it. logger.info('Read in PSF image from bzipped FITS file') # Setup the image: full_image = galsim.ImageF(image_size, image_size, scale=pixel_scale) # The default convention for indexing an image is to follow the FITS standard where the # lower-left pixel is called (1,1). However, this can be counter-intuitive to people more # used to C or python indexing, where indices start at 0. It is possible to change the # coordinates of the lower-left pixel with the methods `setOrigin`. For this demo, we # switch to 0-based indexing, so the lower-left pixel will be called (0,0). full_image.setOrigin(0, 0) # Get the center of the image in arcsec center = full_image.bounds.trueCenter() * pixel_scale # As for demo10, we use random_seed+nobj for the random numbers required for the # whole image. In this case, both the power spectrum realization and the noise on the # full image we apply later. rng = galsim.BaseDeviate(random_seed + nobj) # We want to make random positions within our image. However, currently for shears from a power # spectrum we first have to get shears on a grid of positions, and then we can choose random # positions within that. So, let's make the grid. We're going to make it as large as the # image, with grid points spaced by 90 arcsec (hence interpolation only happens below 90" # scales, below the interesting scales on which we want the shear power spectrum to be # represented exactly). Lensing engine wants positions in arcsec, so calculate that: ps.buildGrid(grid_spacing=grid_spacing, ngrid=int(image_size_arcsec / grid_spacing) + 1, center=center, rng=rng) logger.info('Made gridded shears') # Now we need to loop over our objects: for k in range(nobj): time1 = time.time() # The usual random number generator using a different seed for each galaxy. ud = galsim.UniformDeviate(random_seed + k) # Choose a random position in the image x = ud() * (image_size - 1) y = ud() * (image_size - 1) # Turn this into a position in arcsec pos = galsim.PositionD(x, y) * pixel_scale # Get the reduced shears and magnification at this point g1, g2, mu = ps.getLensing(pos=pos) # Construct the galaxy: # Select randomly from among our list of galaxies. index = int(ud() * len(gal_list)) gal = gal_list[index] # Draw the size from a plausible size distribution: N(r) ~ r^-3.5 # For this, we use the class DistDeviate which can draw deviates from an arbitrary # probability distribution. This distribution can be defined either as a functional # form as we do here, or as tabulated lists of x and p values, from which the # function is interpolated. distdev = galsim.DistDeviate(ud, function=lambda x: x**-3.5, x_min=1, x_max=5) dilat = distdev() # Use createDilated rather than applyDilation, so we don't change the galaxies in the # original gal_list -- createDilated makes a new copy. gal = gal.createDilated(dilat) # Apply a random rotation theta = ud() * 2.0 * numpy.pi * galsim.radians gal.applyRotation(theta) # Apply the cosmological (reduced) shear and magnification at this position using a single # GSObject method. gal.applyLensing(g1, g2, mu) # Convolve with the PSF. We don't have to include a pixel response explicitly, since the # SDSS PSF image that we are using included the pixel response already. final = galsim.Convolve(psf, gal) # Account for the fractional part of the position: x_nom = x + 0.5 # Because stamp size is even! See discussion in demo9.py y_nom = y + 0.5 ix_nom = int(math.floor(x_nom + 0.5)) iy_nom = int(math.floor(y_nom + 0.5)) offset = galsim.PositionD(x_nom - ix_nom, y_nom - iy_nom) # Draw it with our desired stamp size stamp = galsim.ImageF(stamp_size, stamp_size) final.draw(image=stamp, dx=pixel_scale, offset=offset) # Rescale flux to get the S/N we want. We have to do that before we add it to the big # image, which might have another galaxy near that point (so our S/N calculation would # erroneously include the flux from the other object). # See demo5.py for the math behind this calculation. sn_meas = math.sqrt(numpy.sum(stamp.array**2) / noise_variance) flux_scaling = gal_signal_to_noise / sn_meas stamp *= flux_scaling # Recenter the stamp at the desired position: stamp.setCenter(ix_nom, iy_nom) # Find the overlapping bounds: bounds = stamp.bounds & full_image.bounds full_image[bounds] += stamp[bounds] time2 = time.time() tot_time = time2 - time1 logger.info('Galaxy %d: position relative to corner = %s, t=%f s', k, str(pos), tot_time) # Add correlated noise to the image -- the correlation function comes from the HST COSMOS images # and is described in more detail in the galsim.correlatednoise.getCOSMOSNoise() docstring. # This function requires a FITS file, stored in the GalSim repository, that represents this # correlation information: the path to this file is a required argument. cf_file_name = os.path.join('..', 'examples', 'data', 'acs_I_unrot_sci_20_cf.fits') # Then use this to initialize the correlation function that we will use to add noise to the # full_image. We set the dx_cosmos keyword equal to our pixel scale, so that the noise among # neighboring pixels is correlated at the same level as it was among neighboring pixels in HST # COSMOS. Using the original pixel scale, dx_cosmos=0.03 [arcsec], would leave very little # correlation among our larger 0.2 arcsec pixels. We also set the point (zero-distance) variance # to our desired value. cn = galsim.correlatednoise.getCOSMOSNoise(rng, cf_file_name, dx_cosmos=pixel_scale, variance=noise_variance) # Now add noise according to this correlation function to the full_image. We have to do this # step at the end, rather than adding to individual postage stamps, in order to get the noise # level right in the overlap regions between postage stamps. full_image.addNoise( cn) # Note image must have the right scale, as it does here. logger.info('Added noise to final large image') # Now write the image to disk. It is automatically compressed with Rice compression, # since the filename we provide ends in .fz. full_image.write(file_name) logger.info('Wrote image to %r', file_name)
def main(argv): """ Make images using variable PSF and shear: - The main image is 10 x 10 postage stamps. - Each postage stamp is 48 x 48 pixels. - The second HDU has the corresponding PSF image. - Applied shear is from a power spectrum P(k) ~ k^1.8. - Galaxies are real galaxies oriented in a ring test of 20 each. - The PSF is Gaussian with FWHM, ellipticity and position angle functions of (x,y) - Noise is Poisson using a nominal sky value of 1.e6. """ logging.basicConfig(format="%(message)s", level=logging.INFO, stream=sys.stdout) logger = logging.getLogger("demo10") # Define some parameters we'll use below. # Normally these would be read in from some parameter file. n_tiles = 10 # number of tiles in each direction. stamp_size = 48 # pixels pixel_scale = 0.44 # arcsec / pixel sky_level = 1.e6 # ADU / arcsec^2 # The random seed is used for both the power spectrum realization and the random properties # of the galaxies. random_seed = 3339201 # Make output directory if not already present. if not os.path.isdir('output'): os.mkdir('output') file_name = os.path.join('output', 'power_spectrum.fits') # These will be created for each object below. The values we'll use will be functions # of (x,y) relative to the center of the image. (r = sqrt(x^2+y^2)) # psf_fwhm = 0.9 + 0.5 * (r/100)^2 -- arcsec # psf_e = 0.4 * (r/100)^1.5 -- large value at the edge, so visible by eye. # psf_beta = atan2(y/x) + pi/2 -- tangential pattern gal_dilation = 3 # Make the galaxies a bit larger than their original size. gal_signal_to_noise = 100 # Pretty high. psf_signal_to_noise = 1000 # Even higher. logger.info('Starting demo script 10') # Read in galaxy catalog cat_file_name = 'real_galaxy_catalog_23.5_example.fits' dir = 'data' real_galaxy_catalog = galsim.RealGalaxyCatalog(cat_file_name, dir=dir) logger.info('Read in %d real galaxies from catalog', real_galaxy_catalog.nobjects) # List of IDs to use. We select 5 particularly irregular galaxies for this demo. # Then we'll choose randomly from this list. id_list = [106416, 106731, 108402, 116045, 116448] # Make the 5 galaxies we're going to use here rather than remake them each time. # This means the Fourier transforms of the real galaxy images don't need to be recalculated # each time, so it's a bit more efficient. gal_list = [ galsim.RealGalaxy(real_galaxy_catalog, id=id) for id in id_list ] # Grab the index numbers before we transform them and lose the index attribute. cosmos_index = [gal.index for gal in gal_list] # Make the galaxies a bit larger than their original observed size. gal_list = [gal.dilate(gal_dilation) for gal in gal_list] # Setup the PowerSpectrum object we'll be using: ps = galsim.PowerSpectrum(lambda k: k**1.8) # The argument here is "e_power_function" which defines the E-mode power to use. # There is also a b_power_function if you want to include any B-mode power: # ps = galsim.PowerSpectrum(e_power_function, b_power_function) # You may even omit the e_power_function argument and have a pure B-mode power spectrum. # ps = galsim.PowerSpectrum(b_power_function = b_power_function) # All the random number generator classes derive from BaseDeviate. # When we construct another kind of deviate class from any other # kind of deviate class, the two share the same underlying random number # generator. Sometimes it can be clearer to just construct a BaseDeviate # explicitly and then construct anything else you need from that. # Note: A BaseDeviate cannot be used to generate any values. It can # only be used in the constructor for other kinds of deviates. # The seeds for the objects are random_seed+1..random_seed+nobj. # The seeds for things at the image or file level use random_seed itself. nobj = n_tiles * n_tiles rng = galsim.BaseDeviate(random_seed) # Have the PowerSpectrum object build a grid of shear values for us to use. grid_g1, grid_g2 = ps.buildGrid(grid_spacing=stamp_size * pixel_scale, ngrid=n_tiles, rng=rng) # Setup the images: gal_image = galsim.ImageF(stamp_size * n_tiles, stamp_size * n_tiles) psf_image = galsim.ImageF(stamp_size * n_tiles, stamp_size * n_tiles) # Update the image WCS to use the image center as the origin of the WCS. # The class that acts like a PixelScale except for this offset is called OffsetWCS. im_center = gal_image.true_center wcs = galsim.OffsetWCS(scale=pixel_scale, origin=im_center) gal_image.wcs = wcs psf_image.wcs = wcs # We will place the tiles in a random order. To do this, we make two lists for the # ix and iy values. Then we apply a random permutation to the lists (in tandem). ix_list = [] iy_list = [] for ix in range(n_tiles): for iy in range(n_tiles): ix_list.append(ix) iy_list.append(iy) # This next function will use the given random number generator, rng, and use it to # randomly permute any number of lists. All lists will have the same random permutation # applied. galsim.random.permute(rng, ix_list, iy_list) # Initialize the OutputCatalog for the truth values names = [ 'gal_num', 'x_image', 'y_image', 'psf_e1', 'psf_e2', 'psf_fwhm', 'cosmos_id', 'cosmos_index', 'theta', 'g1', 'g2', 'shift_x', 'shift_y' ] types = [ int, float, float, float, float, float, str, int, float, float, float, float, float ] truth_catalog = galsim.OutputCatalog(names, types) # Build each postage stamp: for k in range(nobj): # The usual random number generator using a different seed for each galaxy. rng = galsim.BaseDeviate(random_seed + k + 1) # Determine the bounds for this stamp and its center position. ix = ix_list[k] iy = iy_list[k] b = galsim.BoundsI(ix * stamp_size + 1, (ix + 1) * stamp_size, iy * stamp_size + 1, (iy + 1) * stamp_size) sub_gal_image = gal_image[b] sub_psf_image = psf_image[b] pos = wcs.toWorld(b.true_center) # The image comes out as about 211 arcsec across, so we define our variable # parameters in terms of (r/100 arcsec), so roughly the scale size of the image. rsq = (pos.x**2 + pos.y**2) r = math.sqrt(rsq) psf_fwhm = 0.9 + 0.5 * rsq / 100**2 # arcsec psf_e = 0.4 * (r / 100.)**1.5 psf_beta = (math.atan2(pos.y, pos.x) + math.pi / 2) * galsim.radians # Define the PSF profile psf = galsim.Gaussian(fwhm=psf_fwhm) psf_shape = galsim.Shear(e=psf_e, beta=psf_beta) psf = psf.shear(psf_shape) # Define the galaxy profile: # For this demo, we are doing a ring test where the same galaxy profile is drawn at many # orientations stepped uniformly in angle, making a ring in e1-e2 space. # We're drawing each profile at 20 different orientations and then skipping to the # next galaxy in the list. So theta steps by 1/20 * 360 degrees: theta_deg = (k % 20) * 360. / 20 theta = theta_deg * galsim.degrees # The index needs to increment every 20 objects so we use k/20 using integer math. index = k // 20 gal = gal_list[index] # This makes a new copy so we're not changing the object in the gal_list. gal = gal.rotate(theta) # Apply the shear from the power spectrum. We should either turn the gridded shears # grid_g1[iy, ix] and grid_g2[iy, ix] into gridded reduced shears using a utility called # galsim.lensing.theoryToObserved, or use ps.getShear() which by default gets the reduced # shear. ps.getShear() is also more flexible because it can get the shear at positions that # are not on the original grid, as long as they are contained within the bounds of the full # grid. So in this example we'll use ps.getShear(). alt_g1, alt_g2 = ps.getShear(pos) gal = gal.shear(g1=alt_g1, g2=alt_g2) # Apply half-pixel shift in a random direction. shift_r = pixel_scale * 0.5 ud = galsim.UniformDeviate(rng) t = ud() * 2. * math.pi dx = shift_r * math.cos(t) dy = shift_r * math.sin(t) gal = gal.shift(dx, dy) # Make the final image, convolving with the psf final = galsim.Convolve([psf, gal]) # Draw the image final.drawImage(sub_gal_image) # For the PSF image, we don't match the galaxy shift. Rather, we use the offset # parameter to drawImage to apply a random offset of up to 0.5 pixels in each direction. # Note the difference in units between shift and offset. The shift is applied to the # surface brightness profile, so it is in sky coordinates (as all dimension are for # GSObjects), which are arcsec here. The offset though is applied to the image itself, # so it is in pixels. Hence, we don't multiply by pixel_scale. psf_dx = ud() - 0.5 psf_dy = ud() - 0.5 psf_offset = galsim.PositionD(psf_dx, psf_dy) # Draw the PSF image: # We use real space integration over the pixels to avoid some of the # artifacts that can show up with Fourier convolution. # The level of the artifacts is quite low, but when drawing with # so little noise, they are apparent with ds9's zscale viewing. psf.drawImage(sub_psf_image, method='real_space', offset=psf_offset) # Build the noise model: Poisson noise with a given sky level. sky_level_pixel = sky_level * pixel_scale**2 noise = galsim.PoissonNoise(rng, sky_level=sky_level_pixel) # Add noise to the PSF image, using the normal noise model, but scaling the # PSF flux high enough to reach the desired signal-to-noise. # See demo5.py for more info about how this works. sub_psf_image.addNoiseSNR(noise, psf_signal_to_noise) # And also to the galaxy image using its signal-to-noise. sub_gal_image.addNoiseSNR(noise, gal_signal_to_noise) # Add the truth values to the truth catalog row = [ k, b.true_center.x, b.true_center.y, psf_shape.e1, psf_shape.e2, psf_fwhm, id_list[index], cosmos_index[index], (theta_deg % 360.), alt_g1, alt_g2, dx, dy ] truth_catalog.addRow(row) logger.info('Galaxy (%d,%d): position relative to center = %s', ix, iy, str(pos)) logger.info('Done making images of postage stamps') # In this case, we'll attach the truth catalog as an additional HDU in the same file as # the image data. truth_hdu = truth_catalog.writeFitsHdu() # Now write the images to disk. images = [gal_image, psf_image, truth_hdu] # Any items in the "images" list that is already an hdu is just used directly. # The actual images are converted to FITS hdus that contain the image data. galsim.fits.writeMulti(images, file_name) logger.info('Wrote image to %r', file_name)
def main(argv): """ Make images using constant PSF and variable shear: - The main image is 0.2 x 0.2 degrees. - Pixel scale is 0.2 arcsec, hence the image is 3600 x 3600 pixels. - Applied shear is from a cosmological power spectrum read in from file. - The PSF is a real one from SDSS, and corresponds to a convolution of atmospheric PSF, optical PSF, and pixel response, which has been sampled at pixel centers. We used a PSF from SDSS in order to have a PSF profile that could correspond to what you see with a real telescope. However, in order that the galaxy resolution not be too poor, we tell GalSim that the pixel scale for that PSF image is 0.2" rather than 0.396". We are simultaneously lying about the intrinsic size of the PSF and about the pixel scale when we do this. - The galaxy images include some initial correlated noise from the original HST observation. However, we whiten the noise of the final image so the final image has stationary Gaussian noise, rather than correlated noise. """ logging.basicConfig(format="%(message)s", level=logging.INFO, stream=sys.stdout) logger = logging.getLogger("demo11") # Define some parameters we'll use below. # Normally these would be read in from some parameter file. base_stamp_size = 32 # number of pixels in each dimension of galaxy images # This will be scaled up according to the dilation. # Hence the "base_" prefix. pixel_scale = 0.2 # arcsec/pixel image_size = 0.2 * galsim.degrees # size of big image in each dimension image_size = int( (image_size / galsim.arcsec) / pixel_scale) # convert to pixels image_size_arcsec = image_size * pixel_scale # size of big image in each dimension (arcsec) noise_variance = 1.e4 # ADU^2 nobj = 288 # number of galaxies in entire field # (This corresponds to 2 galaxies / arcmin^2) grid_spacing = 90.0 # The spacing between the samples for the power spectrum # realization (arcsec) gal_signal_to_noise = 100 # S/N of each galaxy # random_seed is used for both the power spectrum realization and the random properties # of the galaxies. random_seed = 24783923 file_name = os.path.join('output', 'tabulated_power_spectrum.fits.fz') logger.info('Starting demo script 11') # Read in galaxy catalog cat_file_name = 'real_galaxy_catalog_example.fits' dir = 'data' real_galaxy_catalog = galsim.RealGalaxyCatalog(cat_file_name, dir=dir) logger.info('Read in %d real galaxies from catalog', real_galaxy_catalog.nobjects) # List of IDs to use. We select 5 particularly irregular galaxies for this demo. # Then we'll choose randomly from this list. id_list = [106416, 106731, 108402, 116045, 116448] # We will cache the galaxies that we make in order to save some of the calculations that # happen on construction. In particular, we don't want to recalculate the Fourier transforms # of the real galaxy images, so it's more efficient so make a store of RealGalaxy instances. # We start with them all = None, and fill them in as we make them. gal_list = [None] * len(id_list) # Setup the PowerSpectrum object we'll be using: # To do this, we first have to read in the tabulated shear power spectrum, often denoted # C_ell(ell), where ell has units of inverse angle and C_ell has units of angle^2. However, # GalSim works in the flat-sky approximation, so we use this notation interchangeably with # P(k). GalSim does not calculate shear power spectra for users, who must be able to provide # their own (or use the examples in the repository). # # Here we use a tabulated power spectrum from iCosmo (http://icosmo.org), with the following # cosmological parameters and survey design: # H_0 = 70 km/s/Mpc # Omega_m = 0.25 # Omega_Lambda = 0.75 # w_0 = -1.0 # w_a = 0.0 # n_s = 0.96 # sigma_8 = 0.8 # Smith et al. prescription for the non-linear power spectrum. # Eisenstein & Hu transfer function with wiggles. # Default dN/dz with z_med = 1.0 # The file has, as required, just two columns which are k and P(k). However, iCosmo works in # terms of ell and C_ell; ell is inverse radians and C_ell in radians^2. Since GalSim tends to # work in terms of arcsec, we have to tell it that the inputs are radians^-1 so it can convert # to store in terms of arcsec^-1. pk_file = os.path.join('data', 'cosmo-fid.zmed1.00.out') ps = galsim.PowerSpectrum(pk_file, units=galsim.radians) # The argument here is "e_power_function" which defines the E-mode power to use. logger.info('Set up power spectrum from tabulated P(k)') # Now let's read in the PSF. It's a real SDSS PSF, which means pixel scale of 0.396". However, # the typical seeing is 1.2" and we want to simulate better seeing, so we will just tell GalSim # that the pixel scale is 0.2". We have to be careful with SDSS PSF images, as they have an # added 'soft bias' of 1000 which has been removed before creation of this file, so that the sky # level is properly zero. Also, the file is bzipped, to demonstrate the new capability of # reading in a file that has been compressed in various ways (which GalSim can infer from the # filename). We want to read the image directly into an InterpolatedImage GSObject, so we can # manipulate it as needed (here, the only manipulation needed is convolution). We want a PSF # with flux 1, and we can set the pixel scale using a keyword. psf_file = os.path.join('data', 'example_sdss_psf_sky0.fits.bz2') psf = galsim.InterpolatedImage(psf_file, scale=pixel_scale, flux=1.) logger.info('Read in PSF image from bzipped FITS file') # Setup the image: full_image = galsim.ImageF(image_size, image_size) # The default convention for indexing an image is to follow the FITS standard where the # lower-left pixel is called (1,1). However, this can be counter-intuitive to people more # used to C or python indexing, where indices start at 0. It is possible to change the # coordinates of the lower-left pixel with the methods `setOrigin`. For this demo, we # switch to 0-based indexing, so the lower-left pixel will be called (0,0). full_image.setOrigin(0, 0) # As for demo10, we use random_seed+nobj for the random numbers required for the # whole image. In this case, both the power spectrum realization and the noise on the # full image we apply later. rng = galsim.BaseDeviate(random_seed + nobj) # We want to make random positions within our image. However, currently for shears from a power # spectrum we first have to get shears on a grid of positions, and then we can choose random # positions within that. So, let's make the grid. We're going to make it as large as the # image, with grid points spaced by 90 arcsec (hence interpolation only happens below 90" # scales, below the interesting scales on which we want the shear power spectrum to be # represented exactly). The lensing engine wants positions in arcsec, so calculate that: ps.buildGrid(grid_spacing=grid_spacing, ngrid=int(math.ceil(image_size_arcsec / grid_spacing)), rng=rng.duplicate()) logger.info('Made gridded shears') # We keep track of how much noise is already in the image from the RealGalaxies. # The default initial value is all pixels = 0. noise_image = galsim.ImageF(image_size, image_size) noise_image.setOrigin(0, 0) # Make a slightly non-trivial WCS. We'll use a slightly rotated coordinate system # and center it at the image center. theta = 0.17 * galsim.degrees # ( dudx dudy ) = ( cos(theta) -sin(theta) ) * pixel_scale # ( dvdx dvdy ) ( sin(theta) cos(theta) ) dudx = math.cos(theta.rad()) * pixel_scale dudy = -math.sin(theta.rad()) * pixel_scale dvdx = math.sin(theta.rad()) * pixel_scale dvdy = math.cos(theta.rad()) * pixel_scale image_center = full_image.trueCenter() affine = galsim.AffineTransform(dudx, dudy, dvdx, dvdy, origin=full_image.trueCenter()) # We can also put it on the celestial sphere to give it a bit more realism. # The TAN projection takes a (u,v) coordinate system on a tangent plane and projects # that plane onto the sky using a given point as the tangent point. The tangent # point should be given as a CelestialCoord. sky_center = galsim.CelestialCoord(ra=19.3 * galsim.hours, dec=-33.1 * galsim.degrees) # The third parameter, units, defaults to arcsec, but we make it explicit here. # It sets the angular units of the (u,v) intermediate coordinate system. wcs = galsim.TanWCS(affine, sky_center, units=galsim.arcsec) full_image.wcs = wcs # Now we need to loop over our objects: for k in range(nobj): time1 = time.time() # The usual random number generator using a different seed for each galaxy. ud = galsim.UniformDeviate(random_seed + k) # Draw the size from a plausible size distribution: N(r) ~ r^-2.5 # For this, we use the class DistDeviate which can draw deviates from an arbitrary # probability distribution. This distribution can be defined either as a functional # form as we do here, or as tabulated lists of x and p values, from which the # function is interpolated. # N.B. This calculation logically belongs later in the script, but given how the config # structure works and the fact that we also use this value for the stamp size # calculation, in order to get the output file to match the YAML output file, it # turns out this is where we need to put this use of the random number generator. distdev = galsim.DistDeviate(ud, function=lambda x: x**-2.5, x_min=1, x_max=5) dilat = distdev() # Choose a random position in the image x = ud() * (image_size - 1) y = ud() * (image_size - 1) image_pos = galsim.PositionD(x, y) # Turn this into a position in world coordinates # We leave this in the (u,v) plane, since the PowerSpectrum class is really defined # on the tangent plane, not in (ra,dec). world_pos = affine.toWorld(image_pos) # Get the reduced shears and magnification at this point g1, g2, mu = ps.getLensing(pos=world_pos) # Construct the galaxy: # Select randomly from among our list of galaxies. index = int(ud() * len(gal_list)) gal = gal_list[index] # If we haven't made this galaxy yet, we need to do so. if gal is None: # When whitening the image, we need to make sure the original correlated noise is # present throughout the whole image, otherwise the whitening will do the wrong thing # to the parts of the image that don't include the original image. The RealGalaxy # stores the correct noise profile to use as the gal.noise attribute. This noise # profile is automatically updated as we shear, dilate, convolve, etc. But we need to # tell it how large to pad with this noise by hand. This is a bit complicated for the # code to figure out on its own, so we have to supply the size for noise padding # with the noise_pad_size parameter. # In this case, the postage stamp will be 32 pixels for the undilated galaxies. # We expand the postage stamp as we dilate the galaxies, so that factor doesn't # come into play here. The shear and magnification are not significant, but the # image can be rotated, which adds an extra factor of sqrt(2). So the net required # padded size is # noise_pad_size = 32 * sqrt(2) * 0.2 arcsec/pixel = 9.1 arcsec # We round this up to 10 to be safe. gal = galsim.RealGalaxy(real_galaxy_catalog, rng=ud, id=id_list[index], noise_pad_size=10) # Save it for next time we use this galaxy. gal_list[index] = gal # Apply the dilation we calculated above. gal = gal.dilate(dilat) # Apply a random rotation theta = ud() * 2.0 * numpy.pi * galsim.radians gal = gal.rotate(theta) # Apply the cosmological (reduced) shear and magnification at this position using a single # GSObject method. gal = gal.lens(g1, g2, mu) # Convolve with the PSF. final = galsim.Convolve(psf, gal) # Account for the fractional part of the position: ix = int(math.floor(x + 0.5)) iy = int(math.floor(y + 0.5)) offset = galsim.PositionD(x - ix, y - iy) # Draw it with our desired stamp size (scaled up by the dilation factor): # Note: We make the stamp size odd to make the above calculation of the offset easier. this_stamp_size = 2 * int(math.ceil(base_stamp_size * dilat / 2)) + 1 stamp = galsim.ImageF(this_stamp_size, this_stamp_size) # We use method='no_pixel' here because the SDSS PSF image that we are using includes the # pixel response already. final.drawImage(image=stamp, wcs=wcs.local(image_pos), offset=offset, method='no_pixel') # Now we can whiten or symmetrize the noise on the postage stamp. Galsim automatically # propagates the noise correctly from the initial RealGalaxy object through the applied # shear, distortion, rotation, and convolution into the final object's noise attribute. To # make the noise fully white, use the image.whitenNoise() method. The returned value is the # variance of the Gaussian noise that is present after the whitening process. # # However, this is often overkill for many applications. If it is acceptable to merely end # up with noise with some degree of symmetry (say 4-fold or 8-fold symmetry), then you can # instead have GalSim just add enough noise to make the resulting noise have this kind of # symmetry. Usually this requires adding significantly less additional noise, which means # you can have the resulting total variance be somewhat smaller. The returned variance # corresponds to the zero-lag value of the noise correlation function, which will still have # off-diagonal elements. We can do this step using the image.symmetrizeNoise() method. #new_variance = stamp.whitenNoise(final.noise) new_variance = stamp.symmetrizeNoise(final.noise, 8) # Rescale flux to get the S/N we want. We have to do that before we add it to the big # image, which might have another galaxy near that point (so our S/N calculation would # erroneously include the flux from the other object). # See demo5.py for the math behind this calculation. sn_meas = math.sqrt(numpy.sum(stamp.array**2) / noise_variance) flux_scaling = gal_signal_to_noise / sn_meas stamp *= flux_scaling # This also scales up the current variance by flux_scaling**2. new_variance *= flux_scaling**2 # Recenter the stamp at the desired position: stamp.setCenter(ix, iy) # Find the overlapping bounds: bounds = stamp.bounds & full_image.bounds full_image[bounds] += stamp[bounds] # We need to keep track of how much variance we have currently in the image, so when # we add more noise, we can omit what is already there. noise_image[bounds] += new_variance time2 = time.time() tot_time = time2 - time1 logger.info('Galaxy %d: position relative to center = %s, t=%f s', k, str(world_pos), tot_time) # We already have some noise in the image, but it isn't uniform. So the first thing to do is # to make the Gaussian noise uniform across the whole image. We have a special noise class # that can do this. VariableGaussianNoise takes an image of variance values and applies # Gaussian noise with the corresponding variance to each pixel. # So all we need to do is build an image with how much noise to add to each pixel to get us # up to the maximum value that we already have in the image. max_current_variance = numpy.max(noise_image.array) noise_image = max_current_variance - noise_image vn = galsim.VariableGaussianNoise(rng, noise_image) full_image.addNoise(vn) # Now max_current_variance is the noise level across the full image. We don't want to add that # twice, so subtract off this much from the intended noise that we want to end up in the image. noise_variance -= max_current_variance # Now add Gaussian noise with this variance to the final image. We have to do this step # at the end, rather than adding to individual postage stamps, in order to get the noise # level right in the overlap regions between postage stamps. noise = galsim.GaussianNoise(rng, sigma=math.sqrt(noise_variance)) full_image.addNoise(noise) logger.info('Added noise to final large image') # Now write the image to disk. It is automatically compressed with Rice compression, # since the filename we provide ends in .fz. full_image.write(file_name) logger.info('Wrote image to %r', file_name) # Compute some sky positions of some of the pixels to compare with the values of RA, Dec # that ds9 reports. ds9 always uses (1,1) for the lower left pixel, so the pixel coordinates # of these pixels are different by 1, but you can check that the RA and Dec values are # the same as what GalSim calculates. ra_str = sky_center.ra.hms() dec_str = sky_center.dec.dms() logger.info('Center of image is at RA %sh %sm %ss, DEC %sd %sm %ss', ra_str[0:3], ra_str[3:5], ra_str[5:], dec_str[0:3], dec_str[3:5], dec_str[5:]) for (x, y) in [(0, 0), (0, image_size - 1), (image_size - 1, 0), (image_size - 1, image_size - 1)]: world_pos = wcs.toWorld(galsim.PositionD(x, y)) ra_str = world_pos.ra.hms() dec_str = world_pos.dec.dms() logger.info('Pixel (%4d, %4d) is at RA %sh %sm %ss, DEC %sd %sm %ss', x, y, ra_str[0:3], ra_str[3:5], ra_str[5:], dec_str[0:3], dec_str[3:5], dec_str[5:]) logger.info( 'ds9 reports these pixels as (1,1), (1,3600), etc. with the same RA, Dec.' )