def buildImage(self, config, base, image_num, obj_num, logger): """ Build an Image consisting of a tiled array of postage stamps. @param config The configuration dict for the image field. @param base The base configuration dict. @param image_num The current image number. @param obj_num The first object number in the image. @param logger If given, a logger object to log progress. @returns the final image """ full_xsize = base['image_xsize'] full_ysize = base['image_ysize'] wcs = base['wcs'] full_image = galsim.ImageF(full_xsize, full_ysize) full_image.setOrigin(base['image_origin']) full_image.wcs = wcs full_image.setZero() nobjects = self.nx_tiles * self.ny_tiles # Make a list of ix,iy values according to the specified order: if 'order' in config: order = galsim.config.ParseValue(config,'order',base,str)[0].lower() else: order = 'row' if order.startswith('row'): ix_list = [ ix for iy in range(self.ny_tiles) for ix in range(self.nx_tiles) ] iy_list = [ iy for iy in range(self.ny_tiles) for ix in range(self.nx_tiles) ] elif order.startswith('col'): ix_list = [ ix for ix in range(self.nx_tiles) for iy in range(self.ny_tiles) ] iy_list = [ iy for ix in range(self.nx_tiles) for iy in range(self.ny_tiles) ] elif order.startswith('rand'): ix_list = [ ix for ix in range(self.nx_tiles) for iy in range(self.ny_tiles) ] iy_list = [ iy for ix in range(self.nx_tiles) for iy in range(self.ny_tiles) ] rng = base['rng'] galsim.random.permute(rng, ix_list, iy_list) else: raise ValueError("Invalid order. Must be row, column, or random") # Define a 'image_pos' field so the stamps can set their position appropriately in case # we need it for PowerSpectum or NFWHalo. x0 = (self.stamp_xsize-1)/2. + base['image_origin'].x y0 = (self.stamp_ysize-1)/2. + base['image_origin'].y dx = self.stamp_xsize + self.xborder dy = self.stamp_ysize + self.yborder config['image_pos'] = { 'type' : 'XY' , 'x' : { 'type' : 'List', 'items' : [ x0 + ix*dx for ix in ix_list ] }, 'y' : { 'type' : 'List', 'items' : [ y0 + iy*dy for iy in iy_list ] } } stamps, current_vars = galsim.config.BuildStamps( nobjects, base, logger=logger, obj_num=obj_num, xsize=self.stamp_xsize, ysize=self.stamp_ysize, do_noise=self.do_noise_in_stamps) base['index_key'] = 'image_num' for k in range(nobjects): # This is our signal that the object was skipped. if stamps[k] is None: continue if logger: logger.debug('image %d: full bounds = %s',image_num,str(full_image.bounds)) logger.debug('image %d: stamp %d bounds = %s',image_num,k,str(stamps[k].bounds)) assert full_image.bounds.includes(stamps[k].bounds) b = stamps[k].bounds full_image[b] += stamps[k] # Bring the noise in the image so far up to a flat noise variance # Save the resulting noise variance as self.current_var. self.current_var = 0 if not self.do_noise_in_stamps: if 'noise' in config: self.current_var = galsim.config.FlattenNoiseVariance( base, full_image, stamps, current_vars, logger) return full_image
def test_corr_padding(): """Test for correlated noise padding of InterpolatedImage.""" import time t1 = time.time() # Set up some defaults for tests. decimal_precise=4 decimal_coarse=2 imgfile = 'fits_files/blankimg.fits' orig_nx = 187 orig_ny = 164 big_nx = 319 big_ny = 322 orig_seed = 151241 # Read in some small image of a noise field from HST. # Rescale it to have a decently large amplitude for the purpose of doing these tests. im = 1.e2*galsim.fits.read(imgfile) # Make a CorrrlatedNoise out of it. cn = galsim.CorrelatedNoise(im, galsim.BaseDeviate(orig_seed)) # first, make a noise image orig_img = galsim.ImageF(orig_nx, orig_ny, scale=1.) orig_img.addNoise(cn) # make it into an InterpolatedImage with some zero-padding # (note that default is zero-padding, by factors of several) int_im = galsim.InterpolatedImage(orig_img) # draw into a larger image big_img = galsim.ImageF(big_nx, big_ny) int_im.draw(big_img, scale=1.) # check that variance is diluted by expected amount - should be exact, so check precisely! big_var_expected = np.var(orig_img.array)*float(orig_nx*orig_ny)/(big_nx*big_ny) np.testing.assert_almost_equal(np.var(big_img.array), big_var_expected, decimal=decimal_precise, err_msg='Variance not diluted by expected amount when zero-padding') # make it into an InterpolatedImage with noise-padding int_im = galsim.InterpolatedImage(orig_img, rng = galsim.GaussianDeviate(orig_seed), noise_pad = im, noise_pad_size = max(big_nx,big_ny)) # draw into a larger image big_img = galsim.ImageF(big_nx, big_ny) int_im.draw(big_img, scale=1.) # check that variance is same as original - here, we cannot be too precise because the padded # region is not huge and the comparison will be, well, noisy. np.testing.assert_almost_equal(np.var(big_img.array), np.var(orig_img.array), decimal=decimal_coarse, err_msg='Variance not correct after padding image with correlated noise') # check that if we pass in a RNG, it is actually used to pad with the same noise field # basically, redo all of the above steps and draw into a new image, make sure it's the same as # previous. int_im = galsim.InterpolatedImage( orig_img, rng=galsim.GaussianDeviate(orig_seed), noise_pad=cn, noise_pad_size = max(big_nx,big_ny)) big_img_2 = galsim.ImageF(big_nx, big_ny) int_im.draw(big_img_2, scale=1.) np.testing.assert_array_almost_equal(big_img_2.array, big_img.array, decimal=decimal_precise, err_msg='Cannot reproduce correlated noise-padded image with same choice of seed') # Finally, check inputs: # what if we give it a screwy way of defining the image padding? try: np.testing.assert_raises(ValueError,galsim.InterpolatedImage,orig_img,noise_pad=-1.) except ImportError: print 'The assert_raises tests require nose' # also, check that whether we give it a string, image, or cn, it gives the same noise field # (given the same random seed) infile = 'fits_files/blankimg.fits' inimg = galsim.fits.read(infile) incf = galsim.CorrelatedNoise(inimg, galsim.GaussianDeviate()) # input RNG will be ignored below int_im2 = galsim.InterpolatedImage(orig_img, rng=galsim.GaussianDeviate(orig_seed), noise_pad=inimg, noise_pad_size = max(big_nx,big_ny)) int_im3 = galsim.InterpolatedImage(orig_img, rng=galsim.GaussianDeviate(orig_seed), noise_pad=incf, noise_pad_size = max(big_nx,big_ny)) big_img2 = galsim.ImageF(big_nx, big_ny) big_img3 = galsim.ImageF(big_nx, big_ny) int_im2.draw(big_img2, scale=1.) int_im3.draw(big_img3, scale=1.) np.testing.assert_equal(big_img2.array, big_img3.array, err_msg='Diff ways of specifying correlated noise give diff answers') t2 = time.time() print 'time for %s = %.2f'%(funcname(),t2-t1)
def test_gaussian(): """Test the generation of a specific Gaussian profile against a known result. """ savedImg = galsim.fits.read(os.path.join(imgdir, "gauss_1.fits")) savedImg.setCenter(0, 0) dx = 0.2 myImg = galsim.ImageF(savedImg.bounds, scale=dx) myImg.setCenter(0, 0) gauss = galsim.Gaussian(flux=1, sigma=1) # Reference images were made with old centering, which is equivalent to use_true_center=False. myImg = gauss.drawImage(myImg, scale=dx, method="sb", use_true_center=False) np.testing.assert_array_almost_equal( myImg.array, savedImg.array, 5, err_msg="Using GSObject Gaussian disagrees with expected result") np.testing.assert_almost_equal( myImg.array.sum(dtype=float) * dx**2, myImg.added_flux, 5, err_msg="Gaussian profile GSObject::draw returned wrong added_flux") # Check a non-square image print(myImg.bounds) recImg = galsim.ImageF(45, 66) recImg.setCenter(0, 0) recImg = gauss.drawImage(recImg, scale=dx, method="sb", use_true_center=False) np.testing.assert_array_almost_equal( recImg[savedImg.bounds].array, savedImg.array, 5, err_msg= "Drawing Gaussian on non-square image disagrees with expected result") np.testing.assert_almost_equal( recImg.array.sum(dtype=float) * dx**2, recImg.added_flux, 5, err_msg= "Gaussian profile GSObject::draw on non-square image returned wrong added_flux" ) # Check with default_params gauss = galsim.Gaussian(flux=1, sigma=1, gsparams=default_params) gauss.drawImage(myImg, scale=0.2, method="sb", use_true_center=False) np.testing.assert_array_almost_equal( myImg.array, savedImg.array, 5, err_msg= "Using GSObject Gaussian with default_params disagrees with expected result" ) gauss = galsim.Gaussian(flux=1, sigma=1, gsparams=galsim.GSParams()) gauss.drawImage(myImg, scale=0.2, method="sb", use_true_center=False) np.testing.assert_array_almost_equal( myImg.array, savedImg.array, 5, err_msg= "Using GSObject Gaussian with GSParams() disagrees with expected result" ) # Use non-unity values. gauss = galsim.Gaussian(flux=1.7, sigma=2.3) gsp = galsim.GSParams(xvalue_accuracy=1.e-8, kvalue_accuracy=1.e-8) gauss2 = galsim.Gaussian(flux=1.7, sigma=2.3, gsparams=gsp) assert gauss2 != gauss assert gauss2 == gauss.withGSParams(gsp) assert gauss2 == gauss.withGSParams(xvalue_accuracy=1.e-8, kvalue_accuracy=1.e-8) assert gauss2 == gauss.withGSParams(xvalue_accuracy=1.e-8).withGSParams( kvalue_accuracy=1.e-8) assert gauss2 == gauss.withGSParams(galsim.GSParams(xvalue_accuracy=1.e-8), kvalue_accuracy=1.e-8) assert gauss2 == gauss.withGSParams(gsp).withGSParams( kvalue_accuracy=1.e-8) assert gauss2 == gauss.withGSParams(galsim.GSParams( xvalue_accuracy=1.e-8)).withGSParams(kvalue_accuracy=1.e-8) check_basic(gauss, "Gaussian") # Check invalid parameters assert_raises(TypeError, gauss.withGSParams, xvalue_threshold=1.e-8) assert_raises(TypeError, gauss.withGSParams, xvalue_accuracy=1.e-8, kvalue=1.e-8) # Test photon shooting. do_shoot(gauss, myImg, "Gaussian") # Test kvalues do_kvalue(gauss, myImg, "Gaussian") # Check picklability do_pickle(galsim.GSParams()) # Check GSParams explicitly here too. do_pickle( galsim.GSParams(minimum_fft_size=12, maximum_fft_size=40, folding_threshold=1.e-1, maxk_threshold=2.e-1, kvalue_accuracy=3.e-1, xvalue_accuracy=4.e-1, shoot_accuracy=5.e-1, realspace_relerr=6.e-1, realspace_abserr=7.e-1, integration_relerr=8.e-1, integration_abserr=9.e-1)) do_pickle(gauss, lambda x: x.drawImage(method='no_pixel')) do_pickle(gauss) # Should raise an exception if >=2 radii are provided. assert_raises(TypeError, galsim.Gaussian, sigma=3, half_light_radius=1, fwhm=2) assert_raises(TypeError, galsim.Gaussian, half_light_radius=1, fwhm=2) assert_raises(TypeError, galsim.Gaussian, sigma=3, fwhm=2) assert_raises(TypeError, galsim.Gaussian, sigma=3, half_light_radius=1) # Or none. assert_raises(TypeError, galsim.Gaussian) # Finally, test the noise property for things that don't have any noise set. assert gauss.noise is None # And accessing the attribute from the class should indicate that it is a lazyproperty assert 'lazy_property' in str(galsim.GSObject._noise) # And check that trying to use GSObject directly is an error. assert_raises(NotImplementedError, galsim.GSObject)
def main(argv): """ Make images similar to that done for the Great08 challenge: - Each fits file is 10 x 10 postage stamps. (The real Great08 images are 100x100, but in the interest of making the Demo script a bit quicker, we only build 100 stars and 100 galaxies.) - Each postage stamp is 40 x 40 pixels. - One image is all stars. - A second image is all galaxies. - Applied shear is the same for each galaxy. - Galaxies are oriented randomly, but in pairs to cancel shape noise. - Noise is Poisson using a nominal sky value of 1.e6. - Galaxies are Exponential profiles. """ logging.basicConfig(format="%(message)s", level=logging.INFO, stream=sys.stdout) logger = logging.getLogger("demo5") # Define some parameters we'll use below. # Normally these would be read in from some parameter file. nx_tiles = 10 # ny_tiles = 10 # stamp_xsize = 40 # stamp_ysize = 40 # random_seed = 6424512 # pixel_scale = 1.0 # arcsec / pixel sky_level = 1.e6 # ADU / arcsec^2 # Make output directory if not already present. if not os.path.isdir('output'): os.mkdir('output') psf_file_name = os.path.join('output', 'g08_psf.fits') psf_beta = 3 # psf_fwhm = 2.85 # arcsec (=pixels) psf_trunc = 2. * psf_fwhm # arcsec (=pixels) psf_e1 = -0.019 # psf_e2 = -0.007 # gal_file_name = os.path.join('output', 'g08_gal.fits') gal_signal_to_noise = 200 # Great08 "LowNoise" run gal_resolution = 0.98 # r_gal / r_psf (use r = half_light_radius) # Note: Great08 defined their resolution as r_obs / r_psf, using the convolved # size rather than the pre-convolved size. # Therefore, our r_gal/r_psf = 0.98 approximately corresponds to # their r_obs / r_psf = 1.4. gal_ellip_rms = 0.2 # using "distortion" definition of ellipticity: # e = (a^2-b^2)/(a^2+b^2), where a and b are the # semi-major and semi-minor axes, respectively. gal_ellip_max = 0.6 # Maximum value of e, to avoid getting near e=1. gal_g1 = 0.013 # Applied shear, using normal shear definition: gal_g2 = -0.008 # g = (a-b)/(a+b) shift_radius = 1.0 # arcsec (=pixels) logger.info('Starting demo script 5 using:') logger.info(' - image with %d x %d postage stamps', nx_tiles, ny_tiles) logger.info(' - postage stamps of size %d x %d pixels', stamp_xsize, stamp_ysize) logger.info(' - Moffat PSF (beta = %.1f, FWHM = %.2f, trunc = %.2f),', psf_beta, psf_fwhm, psf_trunc) logger.info(' - PSF ellip = (%.3f,%.3f)', psf_e1, psf_e2) logger.info(' - Exponential galaxies') logger.info(' - Resolution (r_gal / r_psf) = %.2f', gal_resolution) logger.info(' - Ellipticities have rms = %.1f, max = %.1f', gal_ellip_rms, gal_ellip_max) logger.info(' - Applied gravitational shear = (%.3f,%.3f)', gal_g1, gal_g2) logger.info(' - Poisson noise (sky level = %.1e).', sky_level) logger.info(' - Centroid shifts up to = %.2f pixels', shift_radius) # Define the PSF profile psf = galsim.Moffat(beta=psf_beta, fwhm=psf_fwhm, trunc=psf_trunc) # When something can be constructed from multiple sizes, e.g. Moffat, then # you can get any size out even if it wasn't the way the object was constructed. # In this case, we extract the half-light radius, even though we built it with fwhm. # We'll use this later to set the galaxy's half-light radius in terms of a resolution. psf_re = psf.half_light_radius psf = psf.shear(e1=psf_e1, e2=psf_e2) logger.debug('Made PSF profile') # Define the galaxy profile # First figure out the size we need from the resolution gal_re = psf_re * gal_resolution # Make the galaxy profile starting with flux = 1. gal = galsim.Exponential(flux=1., half_light_radius=gal_re) logger.debug('Made galaxy profile') # This profile is placed with different orientations and noise realizations # at each postage stamp in the gal image. gal_image = galsim.ImageF(stamp_xsize * nx_tiles - 1, stamp_ysize * ny_tiles - 1, scale=pixel_scale) psf_image = galsim.ImageF(stamp_xsize * nx_tiles - 1, stamp_ysize * ny_tiles - 1, scale=pixel_scale) shift_radius_sq = shift_radius**2 first_in_pair = True # Make pairs that are rotated by 90 degrees k = 0 for iy in range(ny_tiles): for ix in range(nx_tiles): # The normal procedure for setting random numbers in GalSim is to start a new # random number generator for each object using sequential seed values. # This sounds weird at first (especially if you were indoctrinated by Numerical # Recipes), but for the boost random number generator we use, the "random" # number sequences produced from sequential initial seeds are highly uncorrelated. # # The reason for this procedure is that when we use multiple processes to build # our images, we want to make sure that the results are deterministic regardless # of the way the objects get parcelled out to the different processes. # # Of course, this script isn't using multiple processes, so it isn't required here. # However, we do it nonetheless in order to get the same results as the config # version of this demo script (demo5.yaml). ud = galsim.UniformDeviate(random_seed + k + 1) # Any kind of random number generator can take another RNG as its first # argument rather than a seed value. This makes both objects use the same # underlying generator for their pseudo-random values. gd = galsim.GaussianDeviate(ud, sigma=gal_ellip_rms) # The -1's in the next line are to provide a border of # 1 pixel between postage stamps b = galsim.BoundsI(ix * stamp_xsize + 1, (ix + 1) * stamp_xsize - 1, iy * stamp_ysize + 1, (iy + 1) * stamp_ysize - 1) sub_gal_image = gal_image[b] sub_psf_image = psf_image[b] # Great08 randomized the locations of the two galaxies in each pair, # but for simplicity, we just do them in sequential postage stamps. if first_in_pair: # Use a random orientation: beta = ud() * 2. * math.pi * galsim.radians # Determine the ellipticity to use for this galaxy. ellip = 1 while (ellip > gal_ellip_max): # Don't do `ellip = math.fabs(gd())` # Python basically implements this as a macro, so gd() is called twice! val = gd() ellip = math.fabs(val) # Make a new copy of the galaxy with an applied e1/e2-type distortion # by specifying the ellipticity and a real-space position angle ellip_gal = gal.shear(e=ellip, beta=beta) first_in_pair = False else: # Use the previous ellip_gal profile and rotate it by 90 degrees ellip_gal = ellip_gal.rotate(90 * galsim.degrees) first_in_pair = True # Apply the gravitational reduced shear by specifying g1/g2 this_gal = ellip_gal.shear(g1=gal_g1, g2=gal_g2) # Apply a random shift_radius: rsq = 2 * shift_radius_sq while (rsq > shift_radius_sq): dx = (2 * ud() - 1) * shift_radius dy = (2 * ud() - 1) * shift_radius rsq = dx**2 + dy**2 this_gal = this_gal.shift(dx, dy) # Note that the shifted psf that we create here is purely for the purpose of being able # to draw a separate, shifted psf image. We do not use it when convolving the galaxy # with the psf. this_psf = psf.shift(dx, dy) # Make the final image, convolving with the (unshifted) psf final_gal = galsim.Convolve([psf, this_gal]) # Draw the image final_gal.drawImage(sub_gal_image) # Now add an appropriate amount of noise to get our desired S/N # There are lots of definitions of S/N, but here is the one used by Great08 # We use a weighted integral of the flux: # S = sum W(x,y) I(x,y) / sum W(x,y) # N^2 = Var(S) = sum W(x,y)^2 Var(I(x,y)) / (sum W(x,y))^2 # Now we assume that Var(I(x,y)) is constant so # Var(I(x,y)) = noise_var # We also assume that we are using a matched filter for W, so W(x,y) = I(x,y). # Then a few things cancel and we find that # S/N = sqrt( sum I(x,y)^2 / noise_var ) # # The above procedure is encapsulated in the function image.addNoiseSNR which # sets the flux appropriately given the variance of the noise model. # In our case, noise_var = sky_level_pixel sky_level_pixel = sky_level * pixel_scale**2 noise = galsim.PoissonNoise(ud, sky_level=sky_level_pixel) sub_gal_image.addNoiseSNR(noise, gal_signal_to_noise) # Draw the PSF image # No noise on PSF images. Just draw it as is. this_psf.drawImage(sub_psf_image) # For first instance, measure moments if ix == 0 and iy == 0: psf_shape = sub_psf_image.FindAdaptiveMom() temp_e = psf_shape.observed_shape.e if temp_e > 0.0: g_to_e = psf_shape.observed_shape.g / temp_e else: g_to_e = 0.0 logger.info( 'Measured best-fit elliptical Gaussian for first PSF image: ' ) logger.info(' g1, g2, sigma = %7.4f, %7.4f, %7.4f (pixels)', g_to_e * psf_shape.observed_shape.e1, g_to_e * psf_shape.observed_shape.e2, psf_shape.moments_sigma) x = b.center.x y = b.center.y logger.info( 'Galaxy (%d,%d): center = (%.0f,%0.f) (e,beta) = (%.4f,%.3f)', ix, iy, x, y, ellip, beta / galsim.radians) k = k + 1 logger.info('Done making images of postage stamps') # Now write the images to disk. psf_image.write(psf_file_name) logger.info('Wrote PSF file %s', psf_file_name) gal_image.write(gal_file_name) logger.info('Wrote image to %r', gal_file_name) # using %r adds quotes around filename for us
def test_uncorr_padding(): """Test for uncorrelated noise padding of InterpolatedImage.""" import time t1 = time.time() # Set up some defaults: use weird image sizes / shapes and noise variances. decimal_precise=5 decimal_coarse=2 orig_nx = 147 orig_ny = 174 noise_var = 1.73 big_nx = 519 big_ny = 482 orig_seed = 151241 # first, make a noise image orig_img = galsim.ImageF(orig_nx, orig_ny, scale=1.) gd = galsim.GaussianDeviate(orig_seed, mean=0., sigma=np.sqrt(noise_var)) orig_img.addNoise(galsim.DeviateNoise(gd)) # make it into an InterpolatedImage with some zero-padding # (note that default is zero-padding, by factors of several) int_im = galsim.InterpolatedImage(orig_img) # draw into a larger image big_img = galsim.ImageF(big_nx, big_ny) int_im.draw(big_img, scale=1.) # check that variance is diluted by expected amount - should be exact, so check precisely! # Note that this only works if the big image has the same even/odd-ness in the two sizes. # Otherwise the center of the original image will fall between pixels in the big image. # Then the variance will be smoothed somewhat by the interpolant. big_var_expected = np.var(orig_img.array)*float(orig_nx*orig_ny)/(big_nx*big_ny) np.testing.assert_almost_equal( np.var(big_img.array), big_var_expected, decimal=decimal_precise, err_msg='Variance not diluted by expected amount when zero-padding') # make it into an InterpolatedImage with noise-padding int_im = galsim.InterpolatedImage(orig_img, noise_pad=noise_var, noise_pad_size=max(big_nx,big_ny), rng = galsim.GaussianDeviate(orig_seed)) # draw into a larger image big_img = galsim.ImageF(big_nx, big_ny) int_im.draw(big_img, scale=1.) # check that variance is same as original - here, we cannot be too precise because the padded # region is not huge and the comparison will be, well, noisy. np.testing.assert_almost_equal( np.var(big_img.array), noise_var, decimal=decimal_coarse, err_msg='Variance not correct after padding image with noise') # check that if we pass in a RNG, it is actually used to pad with the same noise field # basically, redo all of the above steps and draw into a new image, make sure it's the same as # previous. int_im = galsim.InterpolatedImage(orig_img, noise_pad=noise_var, noise_pad_size=max(big_nx,big_ny), rng = galsim.GaussianDeviate(orig_seed)) big_img_2 = galsim.ImageF(big_nx, big_ny) int_im.draw(big_img_2, scale=1.) np.testing.assert_array_almost_equal( big_img_2.array, big_img.array, decimal=decimal_precise, err_msg='Cannot reproduce noise-padded image with same choice of seed') # Finally check inputs: what if we give it an input variance that is neg? A list? try: np.testing.assert_raises(ValueError,galsim.InterpolatedImage,orig_img,noise_pad=-1.) except ImportError: print 'The assert_raises tests require nose' t2 = time.time() print 'time for %s = %.2f'%(funcname(),t2-t1)
def test_masks(): """Test that moments and shear estimation routines respond appropriately to masks.""" import time t1 = time.time() # set up some toy galaxy and PSF my_sigma = 1.0 my_pixscale = 0.1 my_g1 = 0.15 my_g2 = -0.4 imsize = 256 g = galsim.Gaussian(sigma=my_sigma) p = galsim.Gaussian( sigma=my_sigma ) # the ePSF is Gaussian (kind of silly but it means we can # predict results exactly) g.applyShear(g1=my_g1, g2=my_g2) obj = galsim.Convolve(g, p) im = galsim.ImageF(imsize, imsize) p_im = galsim.ImageF(imsize, imsize) obj.draw(image=im, dx=my_pixscale) p.draw(image=p_im, dx=my_pixscale) # make some screwy weight and badpix images that should cause issues, and check that the # exception is thrown good_weight_im = galsim.ImageI(imsize, imsize, init_value=1) try: ## different size from image weight_im = galsim.ImageI(imsize, 2 * imsize) np.testing.assert_raises(ValueError, galsim.hsm.FindAdaptiveMom, im, weight_im) np.testing.assert_raises(ValueError, galsim.hsm.EstimateShear, im, p_im, weight_im) badpix_im = galsim.ImageI(imsize, 2 * imsize) np.testing.assert_raises(ValueError, galsim.hsm.FindAdaptiveMom, im, badpix_im) np.testing.assert_raises(ValueError, galsim.hsm.EstimateShear, im, p_im, good_weight_im, badpix_im) ## weird values weight_im = galsim.ImageI(imsize, imsize, init_value=-3) np.testing.assert_raises(ValueError, galsim.hsm.FindAdaptiveMom, im, weight_im) np.testing.assert_raises(ValueError, galsim.hsm.EstimateShear, im, p_im, weight_im) ## excludes all pixels weight_im = galsim.ImageI(imsize, imsize) np.testing.assert_raises(RuntimeError, galsim.hsm.FindAdaptiveMom, im, weight_im) np.testing.assert_raises(RuntimeError, galsim.hsm.EstimateShear, im, p_im, weight_im) badpix_im = galsim.ImageI(imsize, imsize, init_value=-1) np.testing.assert_raises(RuntimeError, galsim.hsm.FindAdaptiveMom, im, good_weight_im, badpix_im) np.testing.assert_raises(RuntimeError, galsim.hsm.EstimateShear, im, p_im, good_weight_im, badpix_im) except ImportError: # assert_raises requires nose, which we don't want to force people to install. # So if they are running this without nose, we just skip these tests. pass # check moments, shear without mask resm = im.FindAdaptiveMom() ress = galsim.hsm.EstimateShear(im, p_im) # check moments, shear with weight image that includes all pixels weightall1 = galsim.ImageI(imsize, imsize, init_value=1) resm_weightall1 = im.FindAdaptiveMom(weightall1) ress_weightall1 = galsim.hsm.EstimateShear(im, p_im, weightall1) # We'll do this series of tests a few times, so encapsulate the code here. def check_equal(resm, ress, resm_test, ress_test, tag): np.testing.assert_equal(resm.observed_shape.e1, resm_test.observed_shape.e1, err_msg="e1 from FindAdaptiveMom changes " + tag) np.testing.assert_equal(resm.observed_shape.e2, resm_test.observed_shape.e2, err_msg="e2 from FindAdaptiveMom changes " + tag) np.testing.assert_equal(resm.moments_sigma, resm_test.moments_sigma, err_msg="sigma from FindAdaptiveMom changes " + tag) np.testing.assert_equal( ress.observed_shape.e1, ress_test.observed_shape.e1, err_msg="observed e1 from EstimateShear changes " + tag) np.testing.assert_equal( ress.observed_shape.e2, ress_test.observed_shape.e2, err_msg="observed e2 from EstimateShear changes " + tag) np.testing.assert_equal( ress.moments_sigma, ress_test.moments_sigma, err_msg="observed sigma from EstimateShear changes " + tag) np.testing.assert_equal( ress.corrected_e1, ress_test.corrected_e1, err_msg="corrected e1 from EstimateShear changes " + tag) np.testing.assert_equal( ress.corrected_e2, ress_test.corrected_e2, err_msg="corrected e2 from EstimateShear changes " + tag) np.testing.assert_equal( ress.resolution_factor, ress_test.resolution_factor, err_msg="resolution factor from EstimateShear changes " + tag) check_equal(resm, ress, resm_weightall1, ress_weightall1, "when using inclusive weight") # check moments and shears with mask of edges, should be nearly the same # (this seems dumb, but it's helpful for keeping track of whether the pointers in the C++ code # are being properly updated despite the masks. If we monkey in that code again, it will be a # useful check.) maskedge = galsim.ImageI(imsize, imsize, init_value=1) xmin = maskedge.xmin xmax = maskedge.xmax ymin = maskedge.ymin ymax = maskedge.ymax edgenum = 3 for ind1 in range(xmin, xmax + 1): for ind2 in range(ymin, ymax + 1): if (ind1 <= (xmin + edgenum)) or (ind1 >= (xmax - edgenum)) or ( ind2 <= (ymin + edgenum)) or (ind2 >= (ymax - edgenum)): maskedge.setValue(ind1, ind2, 0) resm_maskedge = im.FindAdaptiveMom(maskedge) ress_maskedge = galsim.hsm.EstimateShear(im, p_im, maskedge) test_decimal = 4 np.testing.assert_almost_equal( resm.observed_shape.e1, resm_maskedge.observed_shape.e1, decimal=test_decimal, err_msg="e1 from FindAdaptiveMom changes when masking edge") np.testing.assert_almost_equal( resm.observed_shape.e2, resm_maskedge.observed_shape.e2, decimal=test_decimal, err_msg="e2 from FindAdaptiveMom changes when masking edge") np.testing.assert_almost_equal( resm.moments_sigma, resm_maskedge.moments_sigma, decimal=test_decimal, err_msg="sigma from FindAdaptiveMom changes when masking edge") np.testing.assert_almost_equal( ress.observed_shape.e1, ress_maskedge.observed_shape.e1, decimal=test_decimal, err_msg="observed e1 from EstimateShear changes when masking edge") np.testing.assert_almost_equal( ress.observed_shape.e2, ress_maskedge.observed_shape.e2, decimal=test_decimal, err_msg="observed e2 from EstimateShear changes when masking edge") np.testing.assert_almost_equal( ress.moments_sigma, ress_maskedge.moments_sigma, decimal=test_decimal, err_msg="observed sigma from EstimateShear changes when masking edge") np.testing.assert_almost_equal( ress.corrected_e1, ress_maskedge.corrected_e1, decimal=test_decimal, err_msg="corrected e1 from EstimateShear changes when masking edge") np.testing.assert_almost_equal( ress.corrected_e2, ress_maskedge.corrected_e2, decimal=test_decimal, err_msg="corrected e2 from EstimateShear changes when masking edge") np.testing.assert_almost_equal( ress.resolution_factor, ress_maskedge.resolution_factor, decimal=test_decimal, err_msg="resolution factor from EstimateShear changes when masking edge" ) # check that results don't change *at all* i.e. using assert_equal when we do this edge masking # in different ways: ## do the same as the previous test, but with weight map that is floats (0.0 or 1.0) maskedge = galsim.ImageF(imsize, imsize, init_value=1.) for ind1 in range(xmin, xmax + 1): for ind2 in range(ymin, ymax + 1): if (ind1 <= (xmin + edgenum)) or (ind1 >= (xmax - edgenum)) or ( ind2 <= (ymin + edgenum)) or (ind2 >= (ymax - edgenum)): maskedge.setValue(ind1, ind2, 0.) resm_maskedge1 = im.FindAdaptiveMom(maskedge) ress_maskedge1 = galsim.hsm.EstimateShear(im, p_im, maskedge) check_equal(resm_maskedge, ress_maskedge, resm_maskedge1, ress_maskedge1, "when masking with floats") ## make the weight map for allowed pixels a nonzero value that also != 1 maskedge = galsim.ImageF(imsize, imsize, init_value=2.3) for ind1 in range(xmin, xmax + 1): for ind2 in range(ymin, ymax + 1): if (ind1 <= (xmin + edgenum)) or (ind1 >= (xmax - edgenum)) or ( ind2 <= (ymin + edgenum)) or (ind2 >= (ymax - edgenum)): maskedge.setValue(ind1, ind2, 0.) resm_maskedge1 = im.FindAdaptiveMom(maskedge) ress_maskedge1 = galsim.hsm.EstimateShear(im, p_im, maskedge) check_equal(resm_maskedge, ress_maskedge, resm_maskedge1, ress_maskedge1, "when masking with floats != 1") ## make the weight map all equal to 1, and use a badpix map with a range of nonzero values maskedge = galsim.ImageI(imsize, imsize, init_value=1) badpixedge = galsim.ImageI(imsize, imsize, init_value=0) for ind1 in range(xmin, xmax + 1): for ind2 in range(ymin, ymax + 1): if (ind1 <= (xmin + edgenum)) or (ind1 >= (xmax - edgenum)) or ( ind2 <= (ymin + edgenum)) or (ind2 >= (ymax - edgenum)): badpixedge.setValue(ind1, ind2, ind1 + 1) resm_maskedge1 = im.FindAdaptiveMom(maskedge, badpixedge) ress_maskedge1 = galsim.hsm.EstimateShear(im, p_im, maskedge, badpixedge) check_equal(resm_maskedge, ress_maskedge, resm_maskedge1, ress_maskedge1, "when masking with badpix") ## same as previous, but with badpix of floats maskedge = galsim.ImageI(imsize, imsize, init_value=1) badpixedge = galsim.ImageF(imsize, imsize, init_value=0.) for ind1 in range(xmin, xmax + 1): for ind2 in range(ymin, ymax + 1): if (ind1 <= (xmin + edgenum)) or (ind1 >= (xmax - edgenum)) or ( ind2 <= (ymin + edgenum)) or (ind2 >= (ymax - edgenum)): badpixedge.setValue(ind1, ind2, float(ind1 + 1)) resm_maskedge1 = im.FindAdaptiveMom(maskedge, badpixedge) ress_maskedge1 = galsim.hsm.EstimateShear(im, p_im, maskedge, badpixedge) check_equal(resm_maskedge, ress_maskedge, resm_maskedge1, ress_maskedge1, "when masking with badpix (floats)") ## do some of the masking using weight map, and the rest using badpix maskedge = galsim.ImageI(imsize, imsize, init_value=1) badpixedge = galsim.ImageI(imsize, imsize, init_value=0) meanval = int(0.5 * (xmin + xmax)) for ind1 in range(xmin, xmax + 1): for ind2 in range(ymin, ymax + 1): if (ind1 <= (xmin + edgenum)) or (ind1 >= (xmax - edgenum)) or ( ind2 <= (ymin + edgenum)) or (ind2 >= (ymax - edgenum)): if ind1 < meanval: badpixedge.setValue(ind1, ind2, 1) else: maskedge.setValue(ind1, ind2, 0) resm_maskedge1 = im.FindAdaptiveMom(maskedge, badpixedge) ress_maskedge1 = galsim.hsm.EstimateShear(im, p_im, maskedge, badpixedge) check_equal(resm_maskedge, ress_maskedge, resm_maskedge1, ress_maskedge1, "when masking with badpix and weight map") t2 = time.time() print 'time for %s = %.2f' % (funcname(), t2 - t1)
def main(argv): """ Getting reasonably close to including all the principle features of an image from a ground-based telescope: - Use a bulge plus disk model for the galaxy - Both galaxy components are Sersic profiles (n=3.5 and n=1.5 respectively) - Let the PSF have both atmospheric and optical components. - The atmospheric component is a Kolmogorov spectrum. - The optical component has some defocus, coma, and astigmatism. - Add both Poisson noise to the image and Gaussian read noise. - Let the pixels be slightly distorted relative to the sky. """ gal_flux = 1.e6 # ADU ("Analog-to-digital units", the units of the numbers on a CCD) bulge_n = 3.5 # bulge_re = 2.3 # arcsec disk_n = 1.5 # disk_r0 = 0.85 # arcsec (corresponds to half_light_radius of ~3.7 arcsec) bulge_frac = 0.3 # gal_q = 0.73 # (axis ratio 0 < q < 1) gal_beta = 23 # degrees (position angle on the sky) atmos_fwhm = 2.1 # arcsec atmos_e = 0.13 # atmos_beta = 0.81 # radians opt_defocus = 0.53 # wavelengths opt_a1 = -0.29 # wavelengths opt_a2 = 0.12 # wavelengths opt_c1 = 0.64 # wavelengths opt_c2 = -0.33 # wavelengths opt_obscuration = 0.3 # linear scale size of secondary mirror obscuration lam = 800 # nm NB: don't use lambda - that's a reserved word. tel_diam = 4. # meters pixel_scale = 0.23 # arcsec / pixel image_size = 64 # n x n pixels wcs_g1 = -0.02 # wcs_g2 = 0.01 # sky_level = 2.5e4 # ADU / arcsec^2 gain = 1.7 # photons / ADU read_noise = 0.3 # ADU / pixel random_seed = 1314662 # Initialize the (pseudo-)random number generator that we will be using below. rng = galsim.BaseDeviate(random_seed) # Define the galaxy profile. # Normally Sersic profiles are specified by half-light radius, the radius that # encloses half of the total flux. However, for some purposes, it can be # preferable to instead specify the scale radius, where the surface brightness # drops to 1/e of the central peak value. bulge = galsim.Sersic(bulge_n, half_light_radius=bulge_re) disk = galsim.Sersic(disk_n, scale_radius=disk_r0) # Objects may be multiplied by a scalar (which means scaling the flux) and also # added to each other. gal = bulge_frac * bulge + (1 - bulge_frac) * disk # Could also have written the following, which does the same thing: # gal = galsim.Add([ bulge.withFlux(bulge_frac) , disk.withFlux(1-bulge_frac) ]) # Both syntaxes work with more than two summands as well. # Set the overall flux of the combined object. gal = gal.withFlux(gal_flux) # Set the shape of the galaxy according to axis ratio and position angle # Note: All angles in GalSim must have explicit units. gal_shape = galsim.Shear(q=gal_q, beta=gal_beta * galsim.degrees) gal = gal.shear(gal_shape) # Define the atmospheric part of the PSF. # Note: the flux here is the default flux=1. atmos = galsim.Kolmogorov(fwhm=atmos_fwhm) # For the PSF shape here, we use ellipticity rather than axis ratio. # And the position angle can be either degrees or radians. Here we chose radians. atmos = atmos.shear(e=atmos_e, beta=atmos_beta * galsim.radians) logger.debug('Made atmospheric PSF profile') # Define the optical part of the PSF: # The first argument of OpticalPSF below is lambda/diam (wavelength of light / telescope # diameter), which needs to be in the same units used to specify the image scale. We are using # arcsec for that, so we have to self-consistently use arcsec here, using the following # calculation: lam_over_diam = lam * 1.e-9 / tel_diam # radians lam_over_diam *= 206265 # arcsec # Note that we could also have made GalSim do the conversion for us if we did not know the right # factor: # lam_over_diam = lam * 1.e-9 / tel_diam * galsim.radians # lam_over_diam = lam_over_diam / galsim.arcsec logger.debug('Calculated lambda over diam = %f arcsec', lam_over_diam) # The rest of the values should be given in units of the wavelength of the incident light. optics = galsim.OpticalPSF(lam_over_diam, defocus=opt_defocus, coma1=opt_c1, coma2=opt_c2, astig1=opt_a1, astig2=opt_a2, obscuration=opt_obscuration) logger.debug('Made optical PSF profile') # So far, our coordinate transformation between image and sky coordinates has been just a # scaling of the units between pixels and arcsec, which we have defined as the "pixel scale". # This is fine for many purposes, so we have made it easy to treat the coordinate systems # this way via the `scale` parameter to commands like drawImage. However, in general, the # transformation between the two coordinate systems can be more complicated than that, # including distortions, rotations, variation in pixel size, and so forth. GalSim can # model a number of different "World Coordinate System" (WCS) transformations. See the # docstring for BaseWCS for more information. # In this case, we use a WCS that includes a distortion (specified as g1,g2 in this case), # which we call a ShearWCS. wcs = galsim.ShearWCS(scale=pixel_scale, shear=galsim.Shear(g1=wcs_g1, g2=wcs_g2)) logger.debug('Made the WCS') # Next we will convolve the components in world coordinates. psf = galsim.Convolve([atmos, optics]) final = galsim.Convolve([psf, gal]) logger.debug('Convolved components into final profile') # This time we specify a particular size for the image rather than let GalSim # choose the size automatically. GalSim has several kinds of images that it can use: # ImageF uses 32-bit floats (like a C float, aka numpy.float32) # ImageD uses 64-bit floats (like a C double, aka numpy.float64) # ImageS uses 16-bit integers (usually like a C short, aka numpy.int16) # ImageI uses 32-bit integers (usually like a C int, aka numpy.int32) # If you let the GalSim drawImage command create the image for you, it will create an ImageF. # However, you can make a different type if you prefer. In this case, we still use # ImageF, since 32-bit floats are fine. We just want to set the size explicitly. image = galsim.ImageF(image_size, image_size) # Draw the image with the given WCS. Note that we use wcs rather than scale when the # WCS is more complicated than just a pixel scale. final.drawImage(image=image, wcs=wcs) # Also draw the effective PSF by itself and the optical PSF component alone. image_epsf = galsim.ImageF(image_size, image_size) psf.drawImage(image_epsf, wcs=wcs) # We also draw the optical part of the PSF at its own Nyquist-sampled pixel size # in order to better see the features of the (highly structured) profile. # In this case, we draw a "surface brightness image" using method='sb'. Rather than # integrate the flux over the area of each pixel, this method just samples the surface # brightness value at the locations of the pixel centers. We will encounter a few other # drawing methods as we go through this sequence of demos. cf. demos 7, 8, 10, and 11. image_opticalpsf = optics.drawImage(method='sb') logger.debug('Made image of the profile') # Add a constant sky level to the image. image += sky_level * pixel_scale**2 # This time, we use CCDNoise to model the real noise in a CCD image. It takes a sky level, # gain, and read noise, so it can be a bit more realistic than the simpler GaussianNoise # or PoissonNoise that we used in demos 1 and 2. # # The gain is in units of photons/ADU. Technically, real CCDs quote the gain as e-/ADU. # An ideal CCD has one electron per incident photon, but real CCDs have quantum efficiencies # less than 1, so not every photon triggers an electron. We are essentially folding # the quantum efficiency (and filter transmission and anything else like that) into the gain. # The read_noise value is given as ADU/pixel. This is modeled as a pure Gaussian noise # added to the image after applying the pure Poisson noise. image.addNoise(galsim.CCDNoise(rng, gain=gain, read_noise=read_noise)) # Subtract off the sky. image -= sky_level * pixel_scale**2 logger.debug('Added Gaussian and Poisson noise') # Write the images to files. file_name = os.path.join('output', 'demo3.fits') file_name_epsf = os.path.join('output', 'demo3_epsf.fits') file_name_opticalpsf = os.path.join('output', 'demo3_opticalpsf.fits') image.write(file_name) image_epsf.write(file_name_epsf) image_opticalpsf.write(file_name_opticalpsf) logger.info('Wrote image to %r', file_name) logger.info('Wrote effective PSF image to %r', file_name_epsf) logger.info('Wrote optics-only PSF image (Nyquist sampled) to %r', file_name_opticalpsf) # Check that the HSM package, which is bundled with GalSim, finds a good estimate # of the shear. results = galsim.hsm.EstimateShear(image, image_epsf) logger.info('HSM reports that the image has observed shape and size:') logger.info(' e1 = %.3f, e2 = %.3f, sigma = %.3f (pixels)', results.observed_shape.e1, results.observed_shape.e2, results.moments_sigma) logger.info( 'When carrying out Regaussianization PSF correction, HSM reports') logger.info(' e1, e2 = %.3f, %.3f', results.corrected_e1, results.corrected_e2) logger.info( 'Expected values in the limit that noise and non-Gaussianity are negligible:' ) # Convention for shear addition is to apply the second term initially followed by the first. # So this needs to be the WCS shear + the galaxy shape in that order. total_shape = galsim.Shear(g1=wcs_g1, g2=wcs_g2) + gal_shape logger.info(' e1, e2 = %.3f, %.3f', total_shape.e1, total_shape.e2)
def main(argv): """ Make a fits image cube using real COSMOS galaxies from a catalog describing the training sample. - The number of images in the cube matches the number of rows in the catalog. - Each image size is computed automatically by GalSim based on the Nyquist size. - Both galaxies and stars. - PSF is a double Gaussian, the same for each galaxy. - Galaxies are randomly rotated to remove the imprint of any lensing shears in the COSMOS data. - The same shear is applied to each galaxy. - Noise is Poisson using a nominal sky value of 1.e6 ADU/arcsec^2, the noise in the original COSMOS data. """ logging.basicConfig(format="%(message)s", level=logging.INFO, stream=sys.stdout) logger = logging.getLogger("demo6") # Define some parameters we'll use below. cat_file_name = 'real_galaxy_catalog_23.5_example.fits' dir = 'data' # Make output directory if not already present. if not os.path.isdir('output'): os.mkdir('output') cube_file_name = os.path.join('output', 'cube_real.fits') psf_file_name = os.path.join('output', 'psf_real.fits') random_seed = 1512413 sky_level = 1.e6 # ADU / arcsec^2 pixel_scale = 0.16 # arcsec gal_flux = 1.e5 # arbitrary choice, makes nice (not too) noisy images gal_g1 = -0.027 # gal_g2 = 0.031 # gal_mu = 1.082 # mu = ( (1-kappa)^2 - g1^2 - g2^2 )^-1 psf_inner_fwhm = 0.6 # arcsec psf_outer_fwhm = 2.3 # arcsec psf_inner_fraction = 0.8 # fraction of total PSF flux in the inner Gaussian psf_outer_fraction = 0.2 # fraction of total PSF flux in the inner Gaussian ngal = 100 logger.info('Starting demo script 6 using:') logger.info(' - real galaxies from catalog %r', cat_file_name) logger.info(' - double Gaussian PSF') logger.info(' - pixel scale = %.2f', pixel_scale) logger.info(' - Applied gravitational shear = (%.3f,%.3f)', gal_g1, gal_g2) logger.info(' - Poisson noise (sky level = %.1e).', sky_level) # Read in galaxy catalog # Note: dir is the directory both for the catalog itself and also the directory prefix # for the image files listed in the catalog. # If the images are in a different directory, you may also specify image_dir, which gives # the relative path from dir to wherever the images are located. real_galaxy_catalog = galsim.RealGalaxyCatalog(cat_file_name, dir=dir) logger.info('Read in %d real galaxies from catalog', real_galaxy_catalog.nobjects) # Make the double Gaussian PSF psf1 = galsim.Gaussian(fwhm=psf_inner_fwhm, flux=psf_inner_fraction) psf2 = galsim.Gaussian(fwhm=psf_outer_fwhm, flux=psf_outer_fraction) psf = psf1 + psf2 # Draw the PSF with no noise. psf_image = psf.drawImage(scale=pixel_scale) # write to file psf_image.write(psf_file_name) logger.info('Created PSF and wrote to file %r', psf_file_name) # Build the images all_images = [] for k in range(ngal): logger.debug('Start work on image %d', k) t1 = time.time() # Initialize the random number generator we will be using. rng = galsim.UniformDeviate(random_seed + k + 1) gal = galsim.RealGalaxy(real_galaxy_catalog, index=k, flux=gal_flux) logger.debug(' Read in training sample galaxy and PSF from file') t2 = time.time() # Rotate by a random angle theta = 2. * math.pi * rng() * galsim.radians gal = gal.rotate(theta) # Apply the desired shear gal = gal.shear(g1=gal_g1, g2=gal_g2) # Also apply a magnification mu = ( (1-kappa)^2 - |gamma|^2 )^-1 # This conserves surface brightness, so it scales both the area and flux. gal = gal.magnify(gal_mu) # Make the combined profile final = galsim.Convolve([psf, gal]) # Offset by up to 1/2 pixel in each direction # We had previously (in demo4 and demo5) used shift(dx,dy) as a way to shift the center of # the image. Since that is applied to the galaxy, the units are arcsec (since the galaxy # profile itself doesn't know about the pixel scale). Here, the offset applies to the # drawn image, which does know about the pixel scale, so the units of offset are pixels, # not arcsec. Here, we apply an offset of up to half a pixel in each direction. dx = rng() - 0.5 dy = rng() - 0.5 # Draw the profile if k == 0: # Note that the offset argument may be a galsim.PositionD object or a tuple (dx,dy). im = final.drawImage(scale=pixel_scale, offset=(dx, dy)) xsize, ysize = im.array.shape else: im = galsim.ImageF(xsize, ysize) final.drawImage(im, scale=pixel_scale, offset=(dx, dy)) logger.debug(' Drew image') t3 = time.time() # Add a constant background level background = sky_level * pixel_scale**2 im += background # Add Poisson noise. This time, we don't give a sky_level, since we have already # added it to the image, so we don't want any more added. The sky_level parameter # really defines how much _extra_ sky should be added above what is already in the image. im.addNoise(galsim.PoissonNoise(rng)) logger.debug(' Added Poisson noise') t4 = time.time() # Store that into the list of all images all_images += [im] t5 = time.time() logger.debug(' Times: %f, %f, %f, %f', t2 - t1, t3 - t2, t4 - t3, t5 - t4) logger.info('Image %d: size = %d x %d, total time = %f sec', k, xsize, ysize, t5 - t1) logger.info('Done making images of galaxies') # Now write the image to disk. # We write the images to a fits data cube. galsim.fits.writeCube(all_images, cube_file_name) logger.info('Wrote image to fits data cube %r', cube_file_name)
def galaxy2(sky=0.15, galSNR=6000.0, fwhm=7.0): """Make the second fake galaxy. @param sky: Per pixel sky value, used to get Poisson noise (default=0.2) @param galSNR: The SNR of the galaxy, used to get the noise (default=5000.0) @param fwhm: FWHM of the Gaussian PSF used to convolve the image (default=6.0) ------ Three component edge-on disk galaxy with bulge+disk+halo : Component 1: xCen=420.0, yCen=380; FLux=300; Re=8.0; n=3.0, q=0.8, PA1=45.0 Component 2: xCen=420.0, yCen=380; FLux=500; Re=40.0; n=0.8, q=0.1, PA1=45.0 Component 3: xCen=420.0, yCen=380; FLux=600; Re=35.0; n=1.5, q=0.6, PA1=40.0 Contamination 1: FLux=100; Re=20.0; n=2.5, q=0.7, PA1=0.0 Contamination 2: FLux=200; Re=30.0; n=2.0, q=0.6, PA1=75.0 """ logging.basicConfig(format="%(message)s", level=logging.INFO, stream=sys.stdout) logger = logging.getLogger("galaxy2") # Basic information of the image nx, ny = 800, 800 exptime = 100.0 scale = 1.0 logger.info('Galaxy2: Sky=%6.2f, SNR=%5d', sky, galSNR) # Get the PSF, and save the PSF image psf = getPsf(fwhm, psfFile='galaxy2_psf.fits') # Make a GalSim Image gal2Img = galsim.ImageF(nx, ny) imgCen = gal2Img.bounds.trueCenter() imgWcs = galsim.OffsetWCS(scale=scale, origin=imgCen) gal2Img.wcs = imgWcs # Component 1: flux1 = 300; reff1 = 8.0; nser1 = 3.0; q1 = 0.8; pa1 = 45.0 comp1 = galsim.Sersic( n=3.0, half_light_radius=8.0, flux=(300.0 * exptime)).shear( q=0.8, beta=(0.0 * galsim.degrees)).rotate(45.0 * galsim.degrees).shift( (420.0 - (nx / 2.0)), (380.0 - (ny / 2.0))) # Component 1: flux1 = 500; reff1 = 40.0; nser1 = 0.8; q1 = 0.1; pa1 = 45.0 comp2 = galsim.Sersic(n=0.8, half_light_radius=40.0, flux=(500.0 * exptime)).shear( q=0.10, beta=(0.0 * galsim.degrees)).rotate( 45.0 * galsim.degrees).shift( (420.0 - (nx / 2.0)), (380.0 - (ny / 2.0))) # Component 3: flux1 = 600; reff1 = 35.0; nser1 = 1.5; q1 = 0.6; pa1 = 40.0 comp3 = galsim.Sersic(n=1.5, half_light_radius=35.0, flux=(600.0 * exptime)).shear( q=0.6, beta=(0.0 * galsim.degrees)).rotate( 40.0 * galsim.degrees).shift( (420.0 - (nx / 2.0)), (380.0 - (ny / 2.0))) # Contamination 1: cont1 = galsim.Sersic(n=2.5, half_light_radius=20.0, flux=(100.0 * exptime)).shear( q=0.7, beta=(0.0 * galsim.degrees)).rotate( 0.0 * galsim.degrees).shift(50, 200) # Contamination 2: cont2 = galsim.Sersic(n=2.0, half_light_radius=30.0, flux=(200.0 * exptime)).shear( q=0.6, beta=(0.0 * galsim.degrees)).rotate( 75.0 * galsim.degrees).shift(-300, -190) # Add all components together gal2 = galsim.Add([comp1, comp2, comp3, cont1, cont2]) # Convolution gal2Conv = galsim.Convolve([psf, gal2]) # Draw the image gal2Conv.drawImage(gal2Img, method='no_pixel') # Add Noise rng = random.seed(datetime.now()) noise = galsim.PoissonNoise(rng, sky_level=sky) gal2Img.addNoiseSNR(noise, galSNR) # Save the FITS file gal2File = 'galaxy2_img.fits' logger.info('Write to FITS file : %s', gal2File) galsim.fits.write(gal2Img, gal2File) # Save the PNG picture savePng(gal2Img, pngFile='galaxy2_img.png') return gal2Img
def test_shapelet_adjustments(): """Test that adjusting the Shapelet profile in various ways does the right thing """ import time t1 = time.time() ftypes = [np.float32, np.float64] nx = 128 ny = 128 scale = 0.2 im = galsim.ImageF(nx,ny, scale=scale) sigma = 1.8 order = 6 bvec = [1.3, # n = 0 0.02, 0.03, # n = 1 0.23, -0.19, 0.08, # n = 2 0.01, 0.02, 0.04, -0.03, # n = 3 -0.09, 0.07, -0.11, -0.08, 0.11, # n = 4 -0.03, -0.02, -0.08, 0.01, -0.06, -0.03, # n = 5 0.06, -0.02, 0.00, -0.05, -0.04, 0.01, 0.09 ] # n = 6 ref_shapelet = galsim.Shapelet(sigma=sigma, order=order, bvec=bvec) ref_im = galsim.ImageF(nx,ny) ref_shapelet.drawImage(ref_im, scale=scale) # Test that the Shapelet withFlux does the same thing as the GSObject withFlux gsref_shapelet = galsim.GSObject(ref_shapelet) # Make it opaque to the Shapelet versions gsref_shapelet.withFlux(23.).drawImage(ref_im, method='no_pixel') shapelet = galsim.Shapelet(sigma=sigma, order=order, bvec=bvec) shapelet.withFlux(23.).drawImage(im, method='no_pixel') np.testing.assert_array_almost_equal( im.array, ref_im.array, 6, err_msg="Shapelet withFlux disagrees with GSObject withFlux") # Test that scaling the Shapelet flux does the same thing as the GSObject scaling gsref_shapelet *= 0.23 gsref_shapelet.drawImage(ref_im, method='no_pixel') shapelet *= 0.23 shapelet.drawImage(im, method='no_pixel') np.testing.assert_array_almost_equal( im.array, ref_im.array, 6, err_msg="Shapelet *= 0.23 disagrees with GSObject *= 0.23") # Test that the Shapelet rotate does the same thing as the GSObject rotate gsref_shapelet.rotate(23. * galsim.degrees).drawImage(ref_im, method='no_pixel') shapelet.rotate(23. * galsim.degrees).drawImage(im, method='no_pixel') np.testing.assert_array_almost_equal( im.array, ref_im.array, 6, err_msg="Shapelet rotate disagrees with GSObject rotate") # Test that the Shapelet dilate does the same thing as the GSObject dilate gsref_shapelet.dilate(1.3).drawImage(ref_im, method='no_pixel') shapelet.dilate(1.3).drawImage(im, method='no_pixel') np.testing.assert_array_almost_equal( im.array, ref_im.array, 6, err_msg="Shapelet dilate disagrees with GSObject dilate") # Test that the Shapelet magnify does the same thing as the GSObject magnify gsref_shapelet.magnify(0.8).drawImage(ref_im, method='no_pixel') shapelet.magnify(0.8).drawImage(im, method='no_pixel') np.testing.assert_array_almost_equal( im.array, ref_im.array, 6, err_msg="Shapelet magnify disagrees with GSObject magnify") # Test that lens works on Shapelet gsref_shapelet.lens(-0.05, 0.15, 1.1).drawImage(ref_im, method='no_pixel') shapelet.lens(-0.05, 0.15, 1.1).drawImage(im, method='no_pixel') np.testing.assert_array_almost_equal( im.array, ref_im.array, 6, err_msg="Shapelet lens disagrees with GSObject lens") t2 = time.time() print 'time for %s = %.2f'%(funcname(),t2-t1)
def buildImage(self, config, base, image_num, obj_num, logger): """Build an Image containing multiple objects placed at arbitrary locations. @param config The configuration dict for the image field. @param base The base configuration dict. @param image_num The current image number. @param obj_num The first object number in the image. @param logger If given, a logger object to log progress. @returns the final image and the current noise variance in the image as a tuple """ full_xsize = base['image_xsize'] full_ysize = base['image_ysize'] wcs = base['wcs'] full_image = galsim.ImageF(full_xsize, full_ysize) full_image.setOrigin(base['image_origin']) full_image.wcs = wcs full_image.setZero() base['current_image'] = full_image if 'image_pos' in config and 'world_pos' in config: raise AttributeError( "Both image_pos and world_pos specified for Scattered image.") if 'image_pos' not in config and 'world_pos' not in config: xmin = base['image_origin'].x xmax = xmin + full_xsize - 1 ymin = base['image_origin'].y ymax = ymin + full_ysize - 1 config['image_pos'] = { 'type': 'XY', 'x': { 'type': 'Random', 'min': xmin, 'max': xmax }, 'y': { 'type': 'Random', 'min': ymin, 'max': ymax } } stamps, current_vars = galsim.config.BuildStamps(self.nobjects, base, logger=logger, obj_num=obj_num, do_noise=False) base['index_key'] = 'image_num' for k in range(self.nobjects): # This is our signal that the object was skipped. if stamps[k] is None: continue bounds = stamps[k].bounds & full_image.bounds logger.debug('image %d: full bounds = %s', image_num, str(full_image.bounds)) logger.debug('image %d: stamp %d bounds = %s', image_num, k, str(stamps[k].bounds)) logger.debug('image %d: Overlap = %s', image_num, str(bounds)) if bounds.isDefined(): full_image[bounds] += stamps[k][bounds] else: logger.info( "Object centered at (%d,%d) is entirely off the main image,\n" % (stamps[k].center.x, stamps[k].center.y) + "whose bounds are (%d,%d,%d,%d)." % (full_image.bounds.xmin, full_image.bounds.xmax, full_image.bounds.ymin, full_image.bounds.ymax)) # Bring the image so far up to a flat noise variance current_var = galsim.config.FlattenNoiseVariance( base, full_image, stamps, current_vars, logger) return full_image, current_var
def main(argv): """ Make a fits image cube where each frame has two images of the same galaxy drawn with regular FFT convolution and with photon shooting. We do this for 5 different PSFs and 5 different galaxies, each with 4 different (random) fluxes, sizes, and shapes. """ logging.basicConfig(format="%(message)s", level=logging.INFO, stream=sys.stdout) logger = logging.getLogger("demo7") # To turn off logging: #logger.propagate = False # Define some parameters we'll use below. # Make output directory if not already present. if not os.path.isdir('output'): os.mkdir('output') file_name = os.path.join('output', 'cube_phot.fits.gz') random_seed = 553728 sky_level = 1.e4 # ADU / arcsec^2 pixel_scale = 0.28 # arcsec nx = 64 ny = 64 gal_flux_min = 1.e4 # Range for galaxy flux gal_flux_max = 1.e5 gal_hlr_min = 0.3 # arcsec gal_hlr_max = 1.3 # arcsec gal_e_min = 0. # Range for ellipticity gal_e_max = 0.8 psf_fwhm = 0.65 # arcsec # This script is set up as a comparison between using FFTs for doing the convolutions and # shooting photons. The two methods have trade-offs in speed and accuracy which vary # with the kind of profile being drawn and the S/N of the object, among other factors. # In addition, for each method, there are a number of parameters GalSim uses that control # aspects of the calculation that further affect the speed and accuracy. # # We encapsulate these parameters with an object called GSParams. The default values # are intended to be accurate enough for normal precision shear tests, without sacrificing # too much speed. # # Any PSF or galaxy object can be given a gsparams argument on construction that can # have different values to make the calculation more or less accurate (typically trading # off for speed or memory). # # In this script, we adjust some of the values slightly, just to show you how it works. # You could play around with these values and see what effect they have on the drawn images. # Usually, it requires a pretty drastic change in these parameters for you to be able to # notice the difference by eye. But subtle effects that may impact the shapes of galaxies # can happen well before then. # Type help(galsim.GSParams) for the complete list of parameters and more detailed # documentation, including the default values for each parameter. gsparams = galsim.GSParams( alias_threshold= 1.e-2, # maximum fractional flux that may be aliased around edge of FFT maxk_threshold= 2.e-3, # k-values less than this may be excluded off edge of FFT xvalue_accuracy= 1.e-4, # approximations in real space aim to be this accurate kvalue_accuracy= 1.e-4, # approximations in fourier space aim to be this accurate shoot_accuracy= 1.e-4, # approximations in photon shooting aim to be this accurate minimum_fft_size=64) # minimum size of ffts logger.info('Starting demo script 7') # Make the pixel: pix = galsim.Pixel(xw=pixel_scale) # Make the PSF profiles: psf1 = galsim.Gaussian(fwhm=psf_fwhm, gsparams=gsparams) psf2 = galsim.Moffat(fwhm=psf_fwhm, beta=2.4, gsparams=gsparams) psf3_inner = galsim.Gaussian(fwhm=psf_fwhm, flux=0.8, gsparams=gsparams) psf3_outer = galsim.Gaussian(fwhm=2 * psf_fwhm, flux=0.2, gsparams=gsparams) psf3 = psf3_inner + psf3_outer atmos = galsim.Gaussian(fwhm=psf_fwhm, gsparams=gsparams) # The OpticalPSF and set of Zernike values chosen below correspond to a reasonably well aligned, # smallish ~0.3m / 12 inch diameter telescope with a central obscuration of ~0.12m or 5 inches # diameter, being used in optical wavebands. # In the Noll convention, the value of the Zernike coefficient also gives the RMS optical path # difference across a circular pupil. An RMS difference of ~0.5 or larger indicates that parts # of the wavefront are in fully destructive interference, and so we might expect aberrations to # become strong when Zernike aberrations summed in quadrature approach 0.5 wave. # The aberrations chosen in this case correspond to operating close to a 0.25 wave RMS optical # path difference: optics = galsim.OpticalPSF(lam_over_diam=0.6 * psf_fwhm, obscuration=0.4, defocus=0.06, astig1=0.12, astig2=-0.08, coma1=0.07, coma2=0.04, spher=-0.13, gsparams=gsparams) psf4 = galsim.Convolve([atmos, optics ]) # Convolve inherits the gsparams from the first # item in the list. (Or you can supply a gsparams # argument explicitly if you want to override this.) atmos = galsim.Kolmogorov(fwhm=psf_fwhm, gsparams=gsparams) optics = galsim.Airy(lam_over_diam=0.3 * psf_fwhm, gsparams=gsparams) psf5 = galsim.Convolve([atmos, optics]) psfs = [psf1, psf2, psf3, psf4, psf5] psf_names = [ "Gaussian", "Moffat", "Double Gaussian", "OpticalPSF", "Kolmogorov * Airy" ] psf_times = [0, 0, 0, 0, 0] psf_fft_times = [0, 0, 0, 0, 0] psf_phot_times = [0, 0, 0, 0, 0] # Make the galaxy profiles: gal1 = galsim.Gaussian(half_light_radius=1, gsparams=gsparams) gal2 = galsim.Exponential(half_light_radius=1, gsparams=gsparams) gal3 = galsim.DeVaucouleurs(half_light_radius=1, gsparams=gsparams) gal4 = galsim.Sersic(half_light_radius=1, n=2.5, gsparams=gsparams) # A Sersic profile may be truncated if desired. # The units for this are expected to be arcsec (or specifically -- whatever units # you are using for all the size values as defined by the pixel_scale). bulge = galsim.Sersic(half_light_radius=0.7, n=3.2, trunc=8.5, gsparams=gsparams) disk = galsim.Sersic(half_light_radius=1.2, n=1.5, gsparams=gsparams) gal5 = 0.4 * bulge + 0.6 * disk # Net half-light radius is only approximate for this one. gals = [gal1, gal2, gal3, gal4, gal5] gal_names = [ "Gaussian", "Exponential", "Devaucouleurs", "n=2.5 Sersic", "Bulge + Disk" ] gal_times = [0, 0, 0, 0, 0] gal_fft_times = [0, 0, 0, 0, 0] gal_phot_times = [0, 0, 0, 0, 0] # Other times to keep track of: setup_times = 0 fft_times = 0 phot_times = 0 noise_times = 0 # Loop over combinations of psf, gal, and make 4 random choices for flux, size, shape. all_images = [] k = 0 for ipsf in range(len(psfs)): psf = psfs[ipsf] psf_name = psf_names[ipsf] for igal in range(len(gals)): gal = gals[igal] gal_name = gal_names[igal] for i in range(4): logger.debug('Start work on image %d', i) t1 = time.time() # Initialize the random number generator we will be using. rng = galsim.UniformDeviate(random_seed + k) # Get a new copy, we'll want to keep the original unmodified. gal1 = gal.copy() # Generate random variates: flux = rng() * (gal_flux_max - gal_flux_min) + gal_flux_min gal1.setFlux(flux) hlr = rng() * (gal_hlr_max - gal_hlr_min) + gal_hlr_min gal1.applyDilation(hlr) beta_ellip = rng() * 2 * math.pi * galsim.radians ellip = rng() * (gal_e_max - gal_e_min) + gal_e_min gal_shape = galsim.Shear(e=ellip, beta=beta_ellip) gal1.applyShear(gal_shape) # Build the final object by convolving the galaxy, PSF and pixel response. final = galsim.Convolve([psf, pix, gal1]) # For photon shooting, need a version without the pixel (see below). final_nopix = galsim.Convolve([psf, gal1]) # Create the large, double width output image # Rather than provide a dx= argument to the draw commands, we can also # set the pixel scale in the image constructor. # Note: You can also change it after the construction with im.scale=pixel_scale image = galsim.ImageF(2 * nx + 2, ny, scale=pixel_scale) # Assign the following two "ImageViews", fft_image and phot_image. # Using the syntax below, these are views into the larger image. # Changes/additions to the sub-images referenced by the views are automatically # reflected in the original image. fft_image = image[galsim.BoundsI(1, nx, 1, ny)] phot_image = image[galsim.BoundsI(nx + 3, 2 * nx + 2, 1, ny)] logger.debug( ' Read in training sample galaxy and PSF from file') t2 = time.time() # Draw the profile final.draw(fft_image) logger.debug( ' Drew fft image. Total drawn flux = %f. .flux = %f', fft_image.array.sum(), final.getFlux()) t3 = time.time() # Add Poisson noise sky_level_pixel = sky_level * pixel_scale**2 fft_image.addNoise( galsim.PoissonNoise(rng, sky_level=sky_level_pixel)) t4 = time.time() # The next two lines are just to get the output from this demo script # to match the output from the parsing of demo7.yaml. rng = galsim.UniformDeviate(random_seed + k) rng() rng() rng() rng() # Repeat for photon shooting image. # Photon shooting automatically convolves by the pixel, so we've made sure not # to include it in the profile! final_nopix.drawShoot(phot_image, max_extra_noise=sky_level_pixel / 100, rng=rng) t5 = time.time() # For photon shooting, galaxy already has Poisson noise, so we want to make # sure not to add that noise again! Thus, we just add sky noise, which # is Poisson with the mean = sky_level_pixel pd = galsim.PoissonDeviate(rng, mean=sky_level_pixel) # DeviateNoise just adds the action of the given deviate to every pixel. phot_image.addNoise(galsim.DeviateNoise(pd)) # For PoissonDeviate, the mean is not zero, so for a background-subtracted # image, we need to subtract the mean back off when we are done. phot_image -= sky_level_pixel logger.debug( ' Added Poisson noise. Image fluxes are now %f and %f', fft_image.array.sum(), phot_image.array.sum()) t6 = time.time() # Store that into the list of all images all_images += [image] k = k + 1 logger.info( '%d: %s * %s, flux = %.2e, hlr = %.2f, ellip = (%.2f,%.2f)', k, gal_name, psf_name, flux, hlr, gal_shape.getE1(), gal_shape.getE2()) logger.debug(' Times: %f, %f, %f, %f, %f', t2 - t1, t3 - t2, t4 - t3, t5 - t4, t6 - t5) psf_times[ipsf] += t6 - t1 psf_fft_times[ipsf] += t3 - t2 psf_phot_times[ipsf] += t5 - t4 gal_times[igal] += t6 - t1 gal_fft_times[igal] += t3 - t2 gal_phot_times[igal] += t5 - t4 setup_times += t2 - t1 fft_times += t3 - t2 phot_times += t5 - t4 noise_times += t4 - t3 + t6 - t5 logger.info('Done making images of galaxies') logger.info('') logger.info('Some timing statistics:') logger.info(' Total time for setup steps = %f', setup_times) logger.info(' Total time for regular fft drawing = %f', fft_times) logger.info(' Total time for photon shooting = %f', phot_times) logger.info(' Total time for adding noise = %f', noise_times) logger.info('') logger.info('Breakdown by PSF type:') for ipsf in range(len(psfs)): logger.info(' %s: Total time = %f (fft: %f, phot: %f)', psf_names[ipsf], psf_times[ipsf], psf_fft_times[ipsf], psf_phot_times[ipsf]) logger.info('') logger.info('Breakdown by Galaxy type:') for igal in range(len(gals)): logger.info(' %s: Total time = %f (fft: %f, phot: %f)', gal_names[igal], gal_times[igal], gal_fft_times[igal], gal_phot_times[igal]) logger.info('') # Now write the image to disk. # With any write command, you can optionally compress the file using several compression # schemes: # 'gzip' uses gzip on the full output file. # 'bzip2' uses bzip2 on the full output file. # 'rice' uses rice compression on the image, leaving the fits headers readable. # 'gzip_tile' uses gzip in tiles on the output image, leaving the fits headers readable. # 'hcompress' uses hcompress on the image, but it is only valid for 2-d data, so it # doesn't work for writeCube. # 'plio' uses plio on the image, but it is only valid for positive integer data. # Furthermore, the first three have standard filename extensions associated with them, # so if you don't specify a compression, but the filename ends with '.gz', '.bz2' or '.fz', # the corresponding compression will be selected automatically. # In other words, the `compression='gzip'` specification is actually optional here: galsim.fits.writeCube(all_images, file_name, compression='gzip') logger.info('Wrote fft image to fits data cube %r', file_name)
images = [] for i, index in enumerate(indices): lam = spectrum["lam"][index] nphot = spectrum["nphot"][index] print index, lam, nphot psf = galsim.OpticalPSF(lam=lam, diam=1.2, obscuration=0.29, nstruts=6, flux=1.0) image = galsim.ImageF(config.psfstampsize, config.psfstampsize) psf.drawImage(image=image, scale=0.1 / config.psfoversampling) image *= nphot images.append(image) image = sum(images) totflux = np.sum(image.array) image /= totflux print "Output image flux:", np.sum(image.array) image.write(os.path.join(config.workdir, "psf.fits"))
def test_sersic(): """Test the generation of a specific Sersic profile against a known result. """ # Test Sersic savedImg = galsim.fits.read(os.path.join(imgdir, "sersic_3_1.fits")) dx = 0.2 myImg = galsim.ImageF(savedImg.bounds, scale=dx) myImg.setCenter(0, 0) sersic = galsim.Sersic(n=3, flux=1, half_light_radius=1) sersic.drawImage(myImg, scale=dx, method="sb", use_true_center=False) np.testing.assert_array_almost_equal( myImg.array, savedImg.array, 5, err_msg="Using GSObject Sersic disagrees with expected result") # Check with default_params sersic = galsim.Sersic(n=3, flux=1, half_light_radius=1, gsparams=default_params) sersic.drawImage(myImg, scale=dx, method="sb", use_true_center=False) np.testing.assert_array_almost_equal( myImg.array, savedImg.array, 5, err_msg= "Using GSObject Sersic with default_params disagrees with expected result" ) sersic = galsim.Sersic(n=3, flux=1, half_light_radius=1, gsparams=galsim.GSParams()) sersic.drawImage(myImg, scale=dx, method="sb", use_true_center=False) np.testing.assert_array_almost_equal( myImg.array, savedImg.array, 5, err_msg= "Using GSObject Sersic with GSParams() disagrees with expected result") # Use non-unity values. sersic = galsim.Sersic(n=3, flux=1.7, half_light_radius=2.3) gsp = galsim.GSParams(xvalue_accuracy=1.e-8, kvalue_accuracy=1.e-8) sersic2 = galsim.Sersic(n=3, flux=1.7, half_light_radius=2.3, gsparams=gsp) assert sersic2 != sersic assert sersic2 == sersic.withGSParams(gsp) check_basic(sersic, "Sersic") # Test photon shooting. # Convolve with a small gaussian to smooth out the central peak. sersic2 = galsim.Convolve(sersic, galsim.Gaussian(sigma=0.3)) do_shoot(sersic2, myImg, "Sersic") # Test kvalues do_kvalue(sersic, myImg, "Sersic") # Check picklability do_pickle(sersic, lambda x: x.drawImage(method='no_pixel')) do_pickle(sersic) # Now repeat everything using a truncation. (Above had no truncation.) # Test Truncated Sersic # Don't use an integer truncation, since we don't want the truncation line to pass directly # through the center of a pixel where numerical rounding differences may decide whether the # value is zero or not. # This regression test compares to an image built using the code base at 82259f0 savedImg = galsim.fits.read(os.path.join(imgdir, "sersic_3_1_10.fits")) myImg = galsim.ImageF(savedImg.bounds, scale=dx) myImg.setCenter(0, 0) sersic = galsim.Sersic(n=3, flux=1, half_light_radius=1, trunc=9.99) sersic.drawImage(myImg, scale=dx, method="sb", use_true_center=False) np.testing.assert_array_almost_equal( myImg.array, savedImg.array, 5, err_msg="Using truncated GSObject Sersic disagrees with expected result" ) # Use non-unity values. test_flux = 1.8 sersic = galsim.Sersic(n=3, flux=test_flux, half_light_radius=2.3, trunc=5.9) cen = galsim.PositionD(0, 0) np.testing.assert_equal(sersic.centroid, cen) np.testing.assert_almost_equal(sersic.kValue(cen), (1 + 0j) * test_flux) np.testing.assert_almost_equal(sersic.flux, test_flux) np.testing.assert_almost_equal(sersic.xValue(cen), sersic.max_sb) check_basic(sersic, "Truncated Sersic") # Test photon shooting. # Convolve with a small gaussian to smooth out the central peak. sersic2 = galsim.Convolve(sersic, galsim.Gaussian(sigma=0.3)) do_shoot(sersic2, myImg, "Truncated Sersic") # Test kvalues do_kvalue(sersic, myImg, "Truncated Sersic") # Check picklability do_pickle(sersic, lambda x: x.drawImage(method='no_pixel')) do_pickle(sersic) # Check for normalization consistencies with kValue checks. xValues tested in test_sersic_radii. # For half-light radius specified truncated Sersic, with flux_untruncated flag set sersic = galsim.Sersic(n=3, flux=test_flux, half_light_radius=1, trunc=10, flux_untruncated=True) do_kvalue( sersic, myImg, "Truncated Sersic w/ flux_untruncated, half-light radius specified") # For scale radius specified Sersic sersic = galsim.Sersic(n=3, flux=test_flux, scale_radius=0.05) do_kvalue(sersic, myImg, "Sersic w/ scale radius specified") # For scale radius specified truncated Sersic sersic = galsim.Sersic(n=3, flux=test_flux, scale_radius=0.05, trunc=10) do_kvalue(sersic, myImg, "Truncated Sersic w/ scale radius specified") # For scale radius specified truncated Sersic, with flux_untruncated flag set sersic = galsim.Sersic(n=3, flux=test_flux, scale_radius=0.05, trunc=10, flux_untruncated=True) do_kvalue(sersic, myImg, "Truncated Sersic w/ flux_untruncated, scale radius specified") # Test severely truncated Sersic sersic = galsim.Sersic(n=4, flux=test_flux, half_light_radius=1, trunc=1.45) do_kvalue(sersic, myImg, "Severely truncated n=4 Sersic") # Should raise an exception if both scale_radius and half_light_radius are provided. assert_raises(TypeError, galsim.Sersic, n=1.2, scale_radius=3, half_light_radius=1) assert_raises(TypeError, galsim.Sersic, n=1.2) assert_raises(TypeError, galsim.DeVaucouleurs, scale_radius=3, half_light_radius=1) assert_raises(TypeError, galsim.DeVaucouleurs) # Allowed range is [0.3, 6.2] assert_raises(ValueError, galsim.Sersic, n=0.2, scale_radius=3) assert_raises(ValueError, galsim.Sersic, n=6.3, scale_radius=3) # trunc must be > sqrt(2) * hlr assert_raises(ValueError, galsim.Sersic, n=3, half_light_radius=1, trunc=1.4) assert_raises(ValueError, galsim.DeVaucouleurs, half_light_radius=1, trunc=1.4) # Other errors assert_raises(TypeError, galsim.Sersic, scale_radius=3) assert_raises(ValueError, galsim.Sersic, n=3, scale_radius=3, trunc=-1) assert_raises(ValueError, galsim.DeVaucouleurs, scale_radius=3, trunc=-1)
def test_shapelet_adjustments(): """Test that adjusting the Shapelet profile in various ways does the right thing """ ftypes = [np.float32, np.float64] nx = 128 ny = 128 scale = 0.2 im = galsim.ImageF(nx, ny, scale=scale) sigma = 1.8 order = 6 bvec = [ 1.3, # n = 0 0.02, 0.03, # n = 1 0.23, -0.19, 0.08, # n = 2 0.01, 0.02, 0.04, -0.03, # n = 3 -0.09, 0.07, -0.11, -0.08, 0.11, # n = 4 -0.03, -0.02, -0.08, 0.01, -0.06, -0.03, # n = 5 0.06, -0.02, 0.00, -0.05, -0.04, 0.01, 0.09 ] # n = 6 ref_shapelet = galsim.Shapelet(sigma=sigma, order=order, bvec=bvec) ref_im = galsim.ImageF(nx, ny) ref_shapelet.drawImage(ref_im, scale=scale) # Test PQ and NM access np.testing.assert_equal(ref_shapelet.getPQ(0, 0), (bvec[0], 0)) np.testing.assert_equal(ref_shapelet.getPQ(1, 1), (bvec[5], 0)) np.testing.assert_equal(ref_shapelet.getPQ(1, 2), (bvec[8], -bvec[9])) np.testing.assert_equal(ref_shapelet.getPQ(3, 2), (bvec[19], bvec[20])) np.testing.assert_equal(ref_shapelet.getNM(0, 0), (bvec[0], 0)) np.testing.assert_equal(ref_shapelet.getNM(2, 0), (bvec[5], 0)) np.testing.assert_equal(ref_shapelet.getNM(3, -1), (bvec[8], -bvec[9])) np.testing.assert_equal(ref_shapelet.getNM(5, 1), (bvec[19], bvec[20])) # Test that the Shapelet withFlux does the same thing as the GSObject withFlux gsref_shapelet = galsim.GSObject( ref_shapelet) # Make it opaque to the Shapelet versions gsref_shapelet.withFlux(23.).drawImage(ref_im, method='no_pixel') shapelet = galsim.Shapelet(sigma=sigma, order=order, bvec=bvec) shapelet.withFlux(23.).drawImage(im, method='no_pixel') np.testing.assert_array_almost_equal( im.array, ref_im.array, 6, err_msg="Shapelet withFlux disagrees with GSObject withFlux") # Test that scaling the Shapelet flux does the same thing as the GSObject scaling (gsref_shapelet * 0.23).drawImage(ref_im, method='no_pixel') (shapelet * 0.23).drawImage(im, method='no_pixel') np.testing.assert_array_almost_equal( im.array, ref_im.array, 6, err_msg="Shapelet *= 0.23 disagrees with GSObject *= 0.23") # Test that the Shapelet rotate does the same thing as the GSObject rotate gsref_shapelet.rotate(23. * galsim.degrees).drawImage(ref_im, method='no_pixel') shapelet.rotate(23. * galsim.degrees).drawImage(im, method='no_pixel') np.testing.assert_array_almost_equal( im.array, ref_im.array, 6, err_msg="Shapelet rotate disagrees with GSObject rotate") # Test that the Shapelet dilate does the same thing as the GSObject dilate gsref_shapelet.dilate(1.3).drawImage(ref_im, method='no_pixel') shapelet.dilate(1.3).drawImage(im, method='no_pixel') np.testing.assert_array_almost_equal( im.array, ref_im.array, 6, err_msg="Shapelet dilate disagrees with GSObject dilate") # Test that the Shapelet expand does the same thing as the GSObject expand gsref_shapelet.expand(1.7).drawImage(ref_im, method='no_pixel') shapelet.expand(1.7).drawImage(im, method='no_pixel') np.testing.assert_array_almost_equal( im.array, ref_im.array, 6, err_msg="Shapelet expand disagrees with GSObject expand") # Test that the Shapelet magnify does the same thing as the GSObject magnify gsref_shapelet.magnify(0.8).drawImage(ref_im, method='no_pixel') shapelet.magnify(0.8).drawImage(im, method='no_pixel') np.testing.assert_array_almost_equal( im.array, ref_im.array, 6, err_msg="Shapelet magnify disagrees with GSObject magnify") # Test that lens works on Shapelet gsref_shapelet.lens(-0.05, 0.15, 1.1).drawImage(ref_im, method='no_pixel') shapelet.lens(-0.05, 0.15, 1.1).drawImage(im, method='no_pixel') np.testing.assert_array_almost_equal( im.array, ref_im.array, 6, err_msg="Shapelet lens disagrees with GSObject lens")
k = 0 galarray = [] galarray0 = [] for i in range(100): k += 1 rng = galsim.UniformDeviate(random_seed + k + 1) flux = rng() * (gal_flux_max - gal_flux_min) + gal_flux_min this_gal = gal1.withFlux(flux) hlr = rng() * (gal_hlr_max - gal_hlr_min) + gal_hlr_min this_gal = this_gal.dilate(hlr) this_gal = this_gal.shear(g1=0.5, g2=0.5) final = galsim.Convolve([this_gal, psf]) print(rng()) image0 = galsim.ImageF(2 * nx + 2, ny, scale=pixel_scale) fft_image0 = image0[galsim.BoundsI(1, nx, 1, ny)] phot_image0 = image0[galsim.BoundsI(nx + 3, 2 * nx + 2, 1, ny)] final.drawImage(fft_image0, method='fft') final.drawImage(phot_image0, method='phot') image = galsim.ImageF(2 * nx + 2, ny, scale=pixel_scale) fft_image = image[galsim.BoundsI(1, nx, 1, ny)] phot_image = image[galsim.BoundsI(nx + 3, 2 * nx + 2, 1, ny)] final.drawImage(fft_image, method='fft') sky_level_pixel = sky_level * pixel_scale**2 fft_image.addNoise(galsim.PoissonNoise( rng, sky_level=sky_level_pixel)) #add noise rng = galsim.UniformDeviate(random_seed + k + 1) rng()
def test_shapelet_drawImage(): """Test some measured properties of a drawn shapelet against the supposed true values """ ftypes = [np.float32, np.float64] scale = 0.2 test_flux = 23. im = galsim.ImageF(129, 129, scale=scale) for sigma in [1., 0.3, 2.4]: for order in [0, 2, 8]: bvec = np.zeros(galsim.ShapeletSize(order)) bvec[0] = 1. # N,m = 0,0 k = 0 for n in range(1, order + 1): k += n + 1 if n % 2 == 0: # even n bvec[k] = 0.23 / (n * n) # N,m = n,0 or p,q = n/2,n/2 if n >= 2: bvec[k - 2] = 0.14 / n # N,m = n,2 real part bvec[k - 1] = -0.08 / n # N,m = n,2 imag part else: # odd n if n >= 1: bvec[k - 1] = -0.08 / n**3.2 # N,m = n,1 real part bvec[k] = 0.05 / n**2.1 # N,m = n,1 imag part if n >= 3: bvec[k - 3] = 0.31 / n**4.2 # N,m = n,3 real part bvec[k - 2] = -0.18 / n**3.9 # N,m = n,3 imag part print('shapelet vector = ', bvec) shapelet = galsim.Shapelet(sigma=sigma, order=order, bvec=bvec) check_basic(shapelet, "Shapelet", approx_maxsb=True) # Test normalization (This is normally part of do_shoot. When we eventually # implement photon shooting, we should go back to the normal do_shoot call, # and remove this section.) shapelet = shapelet.withFlux(test_flux) shapelet.drawImage(im) flux = im.array.sum() print('im.sum = ', flux, ' cf. ', test_flux) np.testing.assert_almost_equal( flux / test_flux, 1., 4, err_msg= "Flux normalization for Shapelet disagrees with expected result" ) np.testing.assert_allclose( im.array.max(), shapelet.maxSB() * im.scale**2, rtol=0.1, err_msg="Shapelet maxSB did not match maximum pixel") # Test centroid # Note: this only works if the image has odd sizes. If they are even, then # setCenter doesn't actually set the center to the true center of the image # (since it falls between pixels). im.setCenter(0, 0) x, y = np.meshgrid( np.arange(im.array.shape[0]).astype(float) + im.getXMin(), np.arange(im.array.shape[1]).astype(float) + im.getYMin()) x *= scale y *= scale flux = im.array.sum() mx = (x * im.array).sum() / flux my = (y * im.array).sum() / flux conv = galsim.Convolve([shapelet, galsim.Pixel(scale)]) print('centroid = ', mx, my, ' cf. ', conv.centroid()) np.testing.assert_almost_equal( mx, shapelet.centroid().x, 3, err_msg= "Measured centroid (x) for Shapelet disagrees with expected result" ) np.testing.assert_almost_equal( my, shapelet.centroid().y, 3, err_msg= "Measured centroid (y) for Shapelet disagrees with expected result" )
def compare_dft_vs_photon_object(gsobject, psf_object=None, rng=None, pixel_scale=1., size=512, wmult=4., abs_tol_ellip=1.e-5, abs_tol_size=1.e-5, n_trials_per_iter=32, n_photons_per_trial=1e7, moments=True, hsm=False): """Take an input object (with optional PSF) and render it in two ways comparing results at high precision. Using both photon shooting (via drawShoot()) and Discrete Fourier Transform (via draw()) to render images, we compare the numerical values of adaptive moments estimates of size and ellipticity to check consistency. This function takes actual GSObjects as its input, but because these are not yet picklable this means that the internals cannot be parallelized using the Python multiprocessing module. For a parallelized function, that instead uses a config dictionary to specify the test objects, see the function compare_dft_vs_photon_config() in this module. We generate successive sets of `n_trials_per_iter` photon-shot images, using `n_photons_per_trial` photons in each image, until the standard error on the mean absolute size and ellipticity drop below `abs_tol_size` and `abs_tol_ellip`. We then output a ComparisonShapeData object which stores the results. Note that `n_photons_per_trial` should be large (>~ 1e6) to ensure that any biases detected between the photon shooting and DFT-drawn images are due to numerical differences rather than biases on adaptive moments due to noise itself, a generic feature in this work. This can be verified with a convergence test. @param gsobject The GSObject for which this test is to be performed (prior to PSF convolution if a PSF is also supplied via `psf_object`). Note that this function will automatically handle integration over a Pixel of width `pixel_scale`, so a Pixel should not be included in the supplied `gsobject` (unless you really mean to include it, which will be very rare in normal usage). @param psf_object Optional additional PSF for tests of convolved objects, also a GSObject. Note that this function will automatically handle integration over a Pixel of width `pixel_scale`, so this should not be included in the supplied `psf_object`. [default: None] @param rng A BaseDeviate or derived deviate class instance to provide the pseudo random numbers for the photon shooting. [default: None] @param pixel_scale The pixel scale to use in the test images. [default: 1] @param size The size of the images in the rendering tests - all test images are currently square. [default: 512] @param wmult The `wmult` parameter used in .draw() (see the GSObject .draw() method docs via `help(galsim.GSObject.draw)` for more details). [default: 4] @param abs_tol_ellip The test will keep iterating, adding ever greater numbers of trials, until estimates of the 1-sigma standard error on mean ellipticity moments from photon-shot images are smaller than this param value. [default: 1.e-5] @param abs_tol_size The test will keep iterating, adding ever greater numbers of trials, until estimates of the 1-sigma standard error on mean size moments from photon-shot images are smaller than this param value. [default: 1.e-5] @param n_trials_per_iter Number of trial images used to estimate (or successively re-estimate) the standard error on the delta quantities above for each iteration of the tests. [default: 32] @param n_photons_per_trial Number of photons shot in drawShoot() for each trial. This should be large enough that any noise bias (a.k.a. noise rectification bias) on moments estimates is small. [default: 1e7] @param moments Set True to compare rendered images using AdaptiveMoments estimates of simple observed estimates. [default: True] @param hsm Should the rendered images be compared using HSM shear estimates? (i.e. including a PSF correction for shears) [not implemented] """ import sys import logging import time # Some sanity checks on inputs if hsm is True: if psf_object is None: raise ValueError( 'An input psf_object is required for HSM shear estimate testing.' ) else: # Raise an apologetic exception about the HSM not yet being implemented! raise NotImplementedError('Sorry, HSM tests not yet implemented!') if rng is None: rng = galsim.BaseDeviate() # Then define some convenience functions for handling lists and multiple trial operations def _mean(array_like): return np.mean(np.asarray(array_like)) def _stderr(array_like): return np.std(np.asarray(array_like)) / np.sqrt(len(array_like)) def _shoot_trials_single(gsobject, ntrials, dx, imsize, rng, n_photons): """Convenience function to run `ntrials` and collect the results, uses only a single core. Uses a Python for loop but this is very unlikely to be a rate determining factor provided n_photons is suitably large (>1e6). """ g1obslist = [] g2obslist = [] sigmalist = [] im = galsim.ImageF(imsize, imsize) for i in xrange(ntrials): gsobject.drawShoot(im, dx=dx, n_photons=n_photons, rng=rng) res = im.FindAdaptiveMom() g1obslist.append(res.observed_shape.g1) g2obslist.append(res.observed_shape.g2) sigmalist.append(res.moments_sigma) logging.debug('Completed ' + str(i + 1) + '/' + str(ntrials) + ' trials in this iteration') #im.write('check_shoot_trial'+str(i + 1)) CHECK IMAGE return g1obslist, g2obslist, sigmalist # OK, that's the end of the helper functions-within-helper functions, back to the main unit # Start the timer t1 = time.time() # If a PSF is supplied, do the convolution, otherwise just use the gal_object if psf_object is None: logging.info( 'No psf_object supplied, running tests using input gsobject only') test_object = gsobject else: logging.info( 'Generating test_object by convolving gsobject with input psf_object' ) test_object = galsim.Convolve([gsobject, psf_object]) # Draw the FFT image, only needs to be done once # For the FFT drawn image we need to include the galsim.Pixel, for the photon shooting we don't! test_object_pixelized = galsim.Convolve( [test_object, galsim.Pixel(pixel_scale)]) im_draw = galsim.ImageF(size, size) test_object_pixelized.draw(im_draw, dx=pixel_scale, wmult=wmult) res_draw = im_draw.FindAdaptiveMom() sigma_draw = res_draw.moments_sigma g1obs_draw = res_draw.observed_shape.g1 g2obs_draw = res_draw.observed_shape.g2 # Setup storage lists for the trial shooting results sigma_shoot_list = [] g1obs_shoot_list = [] g2obs_shoot_list = [] sigmaerr = 666. # Slightly kludgy but will not accidentally fail the first `while` condition g1obserr = 666. g2obserr = 666. # Initialize iteration counter itercount = 0 # Then begin while loop, farming out sets of n_trials_per_iter trials until we get the # statistical accuracy we require while (g1obserr > abs_tol_ellip) or (g2obserr > abs_tol_ellip) or ( sigmaerr > abs_tol_size): # Run the trials using helper function g1obs_list_tmp, g2obs_list_tmp, sigma_list_tmp = _shoot_trials_single( test_object, n_trials_per_iter, pixel_scale, size, rng, n_photons_per_trial) # Collect results and calculate new standard error g1obs_shoot_list.extend(g1obs_list_tmp) g2obs_shoot_list.extend(g2obs_list_tmp) sigma_shoot_list.extend(sigma_list_tmp) g1obserr = _stderr(g1obs_shoot_list) g2obserr = _stderr(g2obs_shoot_list) sigmaerr = _stderr(sigma_shoot_list) itercount += 1 sys.stdout.write( "." ) # This doesn't add a carriage return at the end of the line, nice! logging.debug('Completed ' + str(itercount) + ' iterations') logging.debug('(g1obserr, g2obserr, sigmaerr) = ' + str(g1obserr) + ', ' + str(g2obserr) + ', ' + str(sigmaerr)) sys.stdout.write("\n") # Take the runtime and collate results into a ComparisonShapeData runtime = time.time() - t1 results = ComparisonShapeData(g1obs_draw, g2obs_draw, sigma_draw, _mean(g1obs_shoot_list), _mean(g2obs_shoot_list), _mean(sigma_shoot_list), g1obserr, g2obserr, sigmaerr, size, pixel_scale, wmult, itercount, n_trials_per_iter, n_photons_per_trial, runtime, gsobject=gsobject, psf_object=psf_object) logging.info('\n' + str(results)) return results
def test_shearest_shape(): """Test that shear estimation is insensitive to shape of input images.""" # this test can help reveal bugs having to do with x / y indexing issues import time t1 = time.time() # just do test for one particular gaussian g1 = shear_values[1] g2 = shear_values[2] e1_psf = 0.05 e2_psf = -0.04 total_shear = np.sqrt(g1**2 + g2**2) conversion_factor = np.tanh(2.0 * math.atanh(total_shear)) / total_shear distortion_1 = g1 * conversion_factor distortion_2 = g2 * conversion_factor gal = galsim.Exponential(flux=1.0, half_light_radius=1.) gal.applyShear(g1=g1, g2=g2) psf = galsim.Kolmogorov(flux=1.0, fwhm=0.7) psf.applyShear(e1=e1_psf, e2=e2_psf) final = galsim.Convolve([gal, psf]) imsize = [128, 256] for method_index in range(len(correction_methods)): print correction_methods[method_index] save_e1 = -100. save_e2 = -100. for gal_x_imsize in imsize: for gal_y_imsize in imsize: for psf_x_imsize in imsize: for psf_y_imsize in imsize: print gal_x_imsize, gal_y_imsize, psf_x_imsize, psf_y_imsize final_image = galsim.ImageF(gal_x_imsize, gal_y_imsize) epsf_image = galsim.ImageF(psf_x_imsize, psf_y_imsize) final.draw(image=final_image, dx=pixel_scale) psf.draw(image=epsf_image, dx=pixel_scale) result = galsim.hsm.EstimateShear( final_image, epsf_image, shear_est=correction_methods[method_index]) e1 = result.corrected_e1 e2 = result.corrected_e2 # make sure answers don't change as we vary image size tot_e = np.sqrt(save_e1**2 + save_e2**2) if tot_e < 99.: print "Testing!" np.testing.assert_almost_equal( e1, save_e1, err_msg="- incorrect e1", decimal=decimal_shape) np.testing.assert_almost_equal( e2, save_e2, err_msg="- incorrect e2", decimal=decimal_shape) print save_e1, save_e2, e1, e2 save_e1 = e1 save_e2 = e2 t2 = time.time() print 'time for %s = %.2f' % (funcname(), t2 - t1)
def test_deltaFunction(): """Test the generation of a Delta function profile """ # Check construction with no arguments gives expected result delta = galsim.DeltaFunction() np.testing.assert_almost_equal(delta.flux, 1.0) check_basic(delta, "DeltaFunction") do_pickle(delta) # Check with default_params delta = galsim.DeltaFunction(flux=1, gsparams=default_params) np.testing.assert_almost_equal(delta.flux, 1.0) test_flux = 17.9 delta = galsim.DeltaFunction(flux=test_flux) np.testing.assert_almost_equal(delta.flux, test_flux) check_basic(delta, "DeltaFunction") do_pickle(delta) gsp = galsim.GSParams(xvalue_accuracy=1.e-8, kvalue_accuracy=1.e-8) delta2 = galsim.DeltaFunction(flux=test_flux, gsparams=gsp) assert delta2 != delta assert delta2 == delta.withGSParams(gsp) # Test operations with no-ops on DeltaFunction delta_shr = delta.shear(g1=0.3, g2=0.1) np.testing.assert_almost_equal(delta_shr.flux, test_flux) delta_dil = delta.dilate(2.0) np.testing.assert_almost_equal(delta_dil.flux, test_flux) delta_rot = delta.rotate(45 * galsim.radians) np.testing.assert_almost_equal(delta_rot.flux, test_flux) delta_tfm = delta.transform(dudx=1.25, dudy=0., dvdx=0., dvdy=0.8) np.testing.assert_almost_equal(delta_tfm.flux, test_flux) delta_shift = delta.shift(1., 2.) np.testing.assert_almost_equal(delta_shift.flux, test_flux) # These aren't no ops, since they do in fact alter the flux. delta_exp = delta.expand(2.0) np.testing.assert_almost_equal(delta_exp.flux, test_flux * 4) delta_mag = delta.magnify(2.0) np.testing.assert_almost_equal(delta_mag.flux, test_flux * 2) delta_tfm = delta.transform(dudx=1.4, dudy=0.2, dvdx=0.4, dvdy=1.2) np.testing.assert_almost_equal(delta_tfm.flux, test_flux * (1.4 * 1.2 - 0.2 * 0.4)) # Test simple translation of DeltaFunction delta2 = delta.shift(1., 2.) offcen = galsim.PositionD(1, 2) np.testing.assert_equal(delta2.centroid, offcen) assert delta2.xValue(offcen) > 1.e10 np.testing.assert_almost_equal(delta2.xValue(galsim.PositionD(0, 0)), 0) # Test photon shooting. gauss = galsim.Gaussian(sigma=1.0) delta_conv = galsim.Convolve(gauss, delta) myImg = galsim.ImageF() do_shoot(delta_conv, myImg, "Delta Function") # Test kvalues do_kvalue(delta_conv, myImg, "Delta Function")
def main(argv): """ Make a fits image cube using parameters from an input catalog - The number of images in the cube matches the number of rows in the catalog. - Each image size is computed automatically by GalSim based on the Nyquist size. - Only galaxies. No stars. - PSF is Moffat - Each galaxy is bulge plus disk: deVaucouleurs + Exponential. - The catalog's columns are: 0 PSF beta (Moffat exponent) 1 PSF FWHM 2 PSF e1 3 PSF e2 4 PSF trunc 5 Disc half-light-radius 6 Disc e1 7 Disc e2 8 Bulge half-light-radius 9 Bulge e1 10 Bulge e2 11 Galaxy dx (the two components have same center) 12 Galaxy dy - Applied shear is the same for each galaxy - 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("demo4") # Define some parameters we'll use below and make directories if needed. cat_file_name = os.path.join('input', 'galsim_default_input.asc') if not os.path.isdir('output'): os.mkdir('output') multi_file_name = os.path.join('output', 'multi.fits') random_seed = 8241573 sky_level = 1.e6 # ADU / arcsec^2 pixel_scale = 1.0 # arcsec / pixel (size units in input catalog are pixels) gal_flux = 1.e6 # arbitrary choice, makes nice (not too) noisy images gal_g1 = -0.009 # gal_g2 = 0.011 # xsize = 64 # pixels ysize = 64 # pixels logger.info('Starting demo script 4 using:') logger.info(' - parameters taken from catalog %r', cat_file_name) logger.info(' - Moffat PSF (parameters from catalog)') logger.info(' - pixel scale = %.2f', pixel_scale) logger.info(' - Bulge + Disc galaxies (parameters from catalog)') logger.info(' - Applied gravitational shear = (%.3f,%.3f)', gal_g1, gal_g2) logger.info(' - Poisson noise (sky level = %.1e).', sky_level) # Read in the input catalog cat = galsim.Catalog(cat_file_name) # save a list of the galaxy images in the "images" list variable: images = [] for k in range(cat.nobjects): # Initialize the (pseudo-)random number generator that we will be using below. # Use a different random seed for each object to get different noise realizations. # Using sequential random seeds here is safer than it sounds. We use Mersenne Twister # random number generators that are designed to be used with this kind of seeding. # However, to be extra safe, we actually initialize one random number generator with this # seed, generate and throw away two random values with that, and then use the next value # to seed a completely different Mersenne Twister RNG. The result is that successive # RNGs created this way produce very independent random number streams. rng = galsim.BaseDeviate(random_seed + k + 1) # Take the Moffat beta from the first column (called 0) of the input catalog: # Note: cat.get(k,col) returns a string. To get the value as a float, use either # cat.getFloat(k,col) or float(cat.get(k,col)) beta = cat.getFloat(k, 0) # A Moffat's size may be either scale_radius, fwhm, or half_light_radius. # Here we use fwhm, taking from the catalog as well. fwhm = cat.getFloat(k, 1) # A Moffat profile may be truncated if desired # The units for this are expected to be arcsec (or specifically -- whatever units # you are using for all the size values as defined by the pixel_scale). trunc = cat.getFloat(k, 4) # Note: You may omit the flux, since the default is flux=1. psf = galsim.Moffat(beta=beta, fwhm=fwhm, trunc=trunc) # Take the (e1, e2) shape parameters from the catalog as well. psf = psf.shear(e1=cat.getFloat(k, 2), e2=cat.getFloat(k, 3)) # Galaxy is a bulge + disk with parameters taken from the catalog: disk = galsim.Exponential(flux=0.6, half_light_radius=cat.getFloat(k, 5)) disk = disk.shear(e1=cat.getFloat(k, 6), e2=cat.getFloat(k, 7)) bulge = galsim.DeVaucouleurs(flux=0.4, half_light_radius=cat.getFloat(k, 8)) bulge = bulge.shear(e1=cat.getFloat(k, 9), e2=cat.getFloat(k, 10)) # The flux of an Add object is the sum of the component fluxes. # Note that in demo3.py, a similar addition was performed by the binary operator "+". gal = galsim.Add([disk, bulge]) # This flux may be overridden by withFlux. The relative fluxes of the components # remains the same, but the total flux is set to gal_flux. gal = gal.withFlux(gal_flux) gal = gal.shear(g1=gal_g1, g2=gal_g2) # The center of the object is normally placed at the center of the postage stamp image. # You can change that with shift: gal = gal.shift(dx=cat.getFloat(k, 11), dy=cat.getFloat(k, 12)) final = galsim.Convolve([psf, gal]) # Draw the profile image = galsim.ImageF(xsize, ysize) final.drawImage(image, scale=pixel_scale) # Add Poisson noise to the image: image.addNoise(galsim.PoissonNoise(rng, sky_level * pixel_scale**2)) logger.info('Drew image for object at row %d in the input catalog' % k) # Add the image to our list of images images.append(image) # Now write the images to a multi-extension fits file. Each image will be in its own HDU. galsim.fits.writeMulti(images, multi_file_name) logger.info('Images written to multi-extension fits file %r', multi_file_name)
def runSkyModel(config): # image properties data_path = config.get('pipeline', 'data_path') pixel_scale = config.getfloat('skymodel', 'pixel_scale') * galsim.arcsec fov = config.getfloat('skymodel', 'field_of_view') * galsim.arcmin image_size = int((fov / galsim.arcmin) / (pixel_scale / galsim.arcmin)) ra_field = config.get('field', 'field_ra') ra_field_gs = galsim.HMS_Angle(ra_field) dec_field = config.get('field', 'field_dec') dec_field_gs = galsim.DMS_Angle(dec_field) cat_file_name = config.get('field', 'catalogue') print('Loading catalogue from {0} ...'.format(cat_file_name)) cat = fits.getdata(cat_file_name) nobj = len(cat) cat_wcs = ast_wcs.WCS(naxis=2) cat_wcs.wcs.crpix = [image_size / 2, image_size / 2] cat_wcs.wcs.cdelt = [ pixel_scale / galsim.degrees, pixel_scale / galsim.degrees ] cat_wcs.wcs.crval = [0.e0, 0.e0] cat_wcs.wcs.ctype = ['RA---TAN', 'DEC--TAN'] gal_ra = cat['latitude'] gal_dec = cat['longitude'] gal_e1 = cat['e1'] gal_e2 = cat['e2'] gal_flux = cat['I1400'] #mjy gal_r0 = cat['size'] / 2. g1 = 0 g2 = 0 print('...done.') full_image = galsim.ImageF(image_size, image_size, scale=pixel_scale) im_center = full_image.bounds.trueCenter() sky_center = galsim.CelestialCoord(ra=ra_field_gs, dec=dec_field_gs) # - on dx's since the ra axis is flipped. dudx = -pixel_scale / galsim.arcsec dudy = 0. dvdx = 0. dvdy = pixel_scale / galsim.arcsec image_center = full_image.trueCenter() affine = galsim.AffineTransform(dudx, dudy, dvdx, dvdy, origin=full_image.trueCenter()) wcs = galsim.TanWCS(affine, sky_center, units=galsim.arcsec) full_image.wcs = wcs tstart = time.time() nobj = 200 for i in range(nobj): sys.stdout.write('\rAdding source {0} of {1} to skymodel...'.format( i + 1, nobj)) gal = galsim.Exponential(scale_radius=gal_r0[i], flux=gal_flux[i]) ellipticity = galsim.Shear(e1=gal_e1[i], e2=gal_e2[i]) shear = galsim.Shear(g1=g1[i], g2=g2[i]) total_shear = ellipticity + shear gal = gal.shear(total_shear) x, y = cat_wcs.wcs_world2pix(gal_ra[i], gal_dec[i], 0) x = float(x) y = float(y) # Account for the fractional part of the position: ix = int(np.floor(x + 0.5)) iy = int(np.floor(y + 0.5)) offset = galsim.PositionD(x - ix, y - iy) stamp = gal.drawImage(scale=pixel_scale / galsim.arcsec, offset=offset) stamp.setCenter(ix, iy) bounds = stamp.bounds & full_image.bounds full_image[bounds] += stamp[bounds] sys.stdout.flush() tend = time.time() print('\n...done in {0} seconds.'.format(tend - tstart)) all_gals_fname = data_path + config.get('field', 'fitsname') print('Writing image data to {0} ...'.format(all_gals_fname)) image_data = full_image.array write4dImage(all_gals_fname, image_data, pixel_scale / galsim.degrees, obs_ra=ra_field_gs / galsim.degrees, obs_dec=dec_field_gs / galsim.degrees, obs_freq=config.getfloat('observation', 'lowest_frequency')) print('...done.') print('runSkyModel complete.')
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) ) 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=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 / math.cos( dec.rad()) * 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 main(argv): # Where to find and output data. path, filename = os.path.split(__file__) datapath = os.path.abspath(os.path.join(path, "data/")) outpath = os.path.abspath(os.path.join(path, "output/")) # In non-script code, use getLogger(__name__) at module scope instead. logging.basicConfig(format="%(message)s", level=logging.INFO, stream=sys.stdout) logger = logging.getLogger("demo13") # Initialize (pseudo-)random number generator. random_seed = 123456 rng = galsim.BaseDeviate(random_seed) # Generate a Poisson noise model. poisson_noise = galsim.PoissonNoise(rng) logger.info('Poisson noise model created.') # Read in the WFIRST filters, setting an AB zeropoint appropriate for this telescope given its # diameter and (since we didn't use any keyword arguments to modify this) using the typical # exposure time for WFIRST images. filters = wfirst.getBandpasses(AB_zeropoint=True) logger.debug('Read in WFIRST imaging filters.') logger.info('Reading from a parametric COSMOS catalog.') # Read in a galaxy catalog - just a random subsample of 100 galaxies for F814W<23.5 from COSMOS. cat_file_name = 'real_galaxy_catalog_example_fits.fits' dir = 'data' # Use the routine that can take COSMOS real or parametric galaxy information, and tell it we # want parametric galaxies that represent an I<23.5 sample. cat = galsim.COSMOSCatalog(cat_file_name, dir=dir, use_real=False) logger.info('Read in %d galaxies from catalog' % cat.nobjects) # Just use a few galaxies, to save time. Note that we are going to put 4000 galaxy images into # our big image, so if we have n_use=10, each galaxy will appear 400 times. Users who want a # more interesting image with greater variation in the galaxy population can change `n_use` to # something larger (but it should be <=100, the number of galaxies in this small example # catalog). With 4000 galaxies in a 4k x 4k image with the WFIRST pixel scale, the effective # galaxy number density is 74/arcmin^2. This is not the number density that is expected for a # sample that is so bright (I<23.5) but it makes the image more visually interesting. One could # think of it as what you'd get if you added up several images at once, making the images for a # sample that is much deeper have the same S/N as that for an I<23.5 sample in a single image. n_use = 10 n_tot = 4000 # Here we carry out the initial steps that are necessary to get a fully chromatic PSF. We use # the getPSF() routine in the WFIRST module, which knows all about the telescope parameters # (diameter, bandpasses, obscuration, etc.). Note that we arbitrarily choose a single SCA # (Sensor Chip Assembly) rather than all of them, for faster calculations, and use a simple # representation of the struts for faster calculations. To do a more exact calculation of the # chromaticity and pupil plane configuration, remove the `approximate_struts` and the `n_waves` # keyword from the call to getPSF(): use_SCA = 7 # This could be any number from 1...18 logger.info('Doing expensive pre-computation of PSF.') t1 = time.time() logger.setLevel(logging.DEBUG) PSFs = wfirst.getPSF(SCAs=use_SCA, approximate_struts=True, n_waves=10, logger=logger) logger.setLevel(logging.INFO) PSF = PSFs[use_SCA] t2 = time.time() logger.info('Done PSF precomputation in %.1f seconds!' % (t2 - t1)) # Define the size of the postage stamp that we use for each individual galaxy within the larger # image, and for the PSF images. stamp_size = 256 # We choose a particular (RA, dec) location on the sky for our observation. ra_targ = 90. * galsim.degrees dec_targ = -10. * galsim.degrees targ_pos = galsim.CelestialCoord(ra=ra_targ, dec=dec_targ) # Get the WCS for an observation at this position. We are not supplying a date, so the routine # will assume it's the vernal equinox. We are also not supplying a position angle for the # observatory, which means that it will just find the optimal one (the one that has the solar # panels pointed most directly towards the Sun given this targ_pos and date). The output of # this routine is a dict of WCS objects, one for each SCA. We then take the WCS for the SCA # that we are using. wcs_list = wfirst.getWCS(world_pos=targ_pos, SCAs=use_SCA) wcs = wcs_list[use_SCA] # We need to find the center position for this SCA. We'll tell it to give us a CelestialCoord # corresponding to (X, Y) = (wfirst.n_pix/2, wfirst.n_pix/2). SCA_cent_pos = wcs.toWorld( galsim.PositionD(wfirst.n_pix / 2, wfirst.n_pix / 2)) # We randomly distribute points in (X, Y) on the CCD. # If we had a real galaxy catalog with positions in terms of RA, dec we could use wcs.toImage() # to find where those objects should be in terms of (X, Y). pos_rng = galsim.UniformDeviate(random_seed) # Make a list of (X, Y, F814W magnitude, n_rot, flip) values. # (X, Y) give the position of the galaxy centroid (or the center of the postage stamp into which # we draw the galaxy) in the big image. # F814W magnitudes are randomly drawn from the catalog, and are used to create a more realistic # flux distribution for the galaxies instead of just having the 10 flux values for the galaxies # we have chosen to draw. # n_rot says how many 90 degree rotations to include for a given realization of each galaxy, so # it doesn't appear completely identical each time we put it in the image. # flip is a random number that will determine whether we include an x-y flip for this appearance # of the galaxy or not. x_stamp = [] y_stamp = [] mag_stamp = [] n_rot_stamp = [] flip_stamp = [] for i_gal in xrange(n_tot): x_stamp.append(pos_rng() * wfirst.n_pix) y_stamp.append(pos_rng() * wfirst.n_pix) # Note that we could use wcs.toWorld() to get the (RA, dec) for these (x, y) positions. Or, # if we had started with (RA, dec) positions, we could have used wcs.toImage() to get the # CCD coordinates for those positions. mag_stamp.append(cat.param_cat.mag_auto[pos_rng() * cat.nobjects]) n_rot_stamp.append(int(4 * pos_rng())) flip_stamp.append(pos_rng()) # Make the 2-component parametric GSObjects for each object, including chromaticity (roughly # appropriate SEDs per galaxy component, at the appropriate galaxy redshift). Note that since # the PSF is position-independent within the SCA, we can simply do the convolution with that PSF # now instead of using a different one for each position. We also have to include the correct # flux scaling: The catalog returns objects that would be observed by 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. logger.info( 'Processing the objects in the catalog to get GSObject representations' ) # Choose a random set of unique indices in the catalog (will be the same each time script is # run, due to use of the same random seed): rand_indices = [] while len(rand_indices) < n_use: tmp_ind = int(pos_rng() * cat.nobjects) if tmp_ind not in rand_indices: rand_indices.append(tmp_ind) obj_list = cat.makeGalaxy(rand_indices, chromatic=True, gal_type='parametric', deep=True) gal_list = [] hst_eff_area = 2.4**2 * (1. - 0.33**2) wfirst_eff_area = galsim.wfirst.diameter**2 * ( 1. - galsim.wfirst.obscuration**2) flux_scaling = (wfirst_eff_area / hst_eff_area) * wfirst.exptime mag_list = [] for ind in range(len(obj_list)): # First, let's check what magnitude this object has in F814W. We want to do this because # (to inject some variety into our images) we are going to rescale the fluxes in all bands # for different instances of this galaxy in the final image in order to get a reasonable S/N # distribution. So we need to save the original magnitude in F814W, to compare with a # randomly drawn one from the catalog. This is not something that most users would need to # do. mag_list.append( cat.param_cat.mag_auto[cat.orig_index[rand_indices[ind]]]) # Convolve the chromatic galaxy and the chromatic PSF, and rescale flux. final = galsim.Convolve(flux_scaling * obj_list[ind], PSF) logger.debug('Pre-processing for galaxy %d completed.' % ind) gal_list.append(final) # Calculate the sky level for each filter, and draw the PSF and the galaxies through the # filters. for filter_name, filter_ in filters.iteritems(): logger.info('Beginning work for {0}.'.format(filter_name)) # Drawing PSF. Note that the PSF object intrinsically has a flat SED, so if we convolve it # with a galaxy, it will properly take on the SED of the galaxy. For the sake of this demo, # we will simply convolve with a 'star' that has a flat SED and unit flux in this band, so # that the PSF image will be normalized to unit flux. This does mean that the PSF image # being drawn here is not quite the right PSF for the galaxy. Indeed, the PSF for the # galaxy effectively varies within it, since it differs for the bulge and the disk. To make # a real image, one would have to choose SEDs for stars and convolve with a star that has a # reasonable SED, but we just draw with a flat SED for this demo. out_filename = os.path.join(outpath, 'demo13_PSF_{0}.fits'.format(filter_name)) # Approximate a point source. point = galsim.Gaussian(sigma=1.e-8, flux=1.) # Use a flat SED here, but could use something else. A stellar SED for instance. # Or a typical galaxy SED. Depending on your purpose for drawing the PSF. star_sed = galsim.SED(lambda x: 1).withFlux( 1., filter_) # Give it unit flux in this filter. star = galsim.Convolve(point * star_sed, PSF) img_psf = galsim.ImageF(64, 64) star.drawImage(bandpass=filter_, image=img_psf, scale=wfirst.pixel_scale) img_psf.write(out_filename) logger.debug( 'Created PSF with flat SED for {0}-band'.format(filter_name)) # Set up the full image that will contain all the individual galaxy images, with information # about WCS: final_image = galsim.ImageF(wfirst.n_pix, wfirst.n_pix, wcs=wcs) # Draw the galaxies into the image. for i_gal in xrange(n_use): logger.info( 'Drawing image for the object at row %d in the input catalog' % i_gal) # We want to only draw the galaxy once (for speed), not over and over with different # sub-pixel offsets. For this reason we ignore the sub-pixel offset entirely. Note # that we are setting the postage stamp to have the average WFIRST pixel scale. This is # simply an approximation for the purpose of speed; really, one should draw directly # into final_image, which has the appropriate WCS for WFIRST. In that case, the image # of the galaxy might look different in different parts of the detector due to the WCS # (including distortion), and we would have to re-draw each time. To keep the demo # relatively quick, we are just using the approximate average pixel scale and drawing # once. stamp = galsim.Image(stamp_size, stamp_size, scale=wfirst.pixel_scale) gal_list[i_gal].drawImage(filter_, image=stamp) # Have to find where to place it: for i_gal_use in range(i_gal * n_tot / n_use, (i_gal + 1) * n_tot / n_use): # Account for the fractional part of the position: ix = int(math.floor(x_stamp[i_gal_use] + 0.5)) iy = int(math.floor(y_stamp[i_gal_use] + 0.5)) # We don't actually use this offset. offset = galsim.PositionD(x_stamp[i_gal] - ix, y_stamp[i_gal] - iy) # Create a nominal bound for the postage stamp given the integer part of the # position. stamp_bounds = galsim.BoundsI(ix - 0.5 * stamp_size, ix + 0.5 * stamp_size - 1, iy - 0.5 * stamp_size, iy + 0.5 * stamp_size - 1) # Find the overlapping bounds between the large image and the individual postage # stamp. bounds = stamp_bounds & final_image.bounds # Just to inject a bit of variety into the image, so it isn't *quite* as obvious # that we've repeated the same 10 objects over and over, we randomly rotate the # postage stamp by some factor of 90 degrees and possibly include a random flip. if flip_stamp[i_gal_use] > 0.5: new_arr = numpy.ascontiguousarray( numpy.rot90(stamp.array, n_rot_stamp[i_gal_use])) else: new_arr = numpy.ascontiguousarray( numpy.fliplr( numpy.rot90(stamp.array, n_rot_stamp[i_gal_use]))) stamp_rot = galsim.Image(new_arr, scale=stamp.scale) stamp_rot.setOrigin( galsim.PositionI(stamp_bounds.xmin, stamp_bounds.ymin)) # Rescale the flux to match that of a randomly chosen galaxy in the galaxy, but # keeping the same SED as for this particular galaxy. This gives a bit more # variety in the flux values and SNR of the galaxies in the image without having # to render images of many more objects. flux_scaling = 10**(-0.4 * (mag_stamp[i_gal_use] - mag_list[i_gal])) # Copy the image into the right place in the big image. final_image[bounds] += flux_scaling * stamp_rot[bounds] # Now we're done with the per-galaxy drawing for this image. The rest will be done for the # entire image at once. logger.info( 'Postage stamps of all galaxies drawn on a single big image for this filter.' ) logger.info('Adding the sky level, noise and detector non-idealities.') # First we get the amount of zodaical light for a position corresponding to the center of # this SCA. The results are provided in units of e-/arcsec^2, using the default WFIRST # exposure time since we did not explicitly specify one. Then we multiply this by a factor # >1 to account for the amount of stray light that is expected. If we do not provide a date # for the observation, then it will assume that it's the vernal equinox (sun at (0,0) in # ecliptic coordinates) in 2025. sky_level = wfirst.getSkyLevel(filters[filter_name], world_pos=SCA_cent_pos) sky_level *= (1.0 + wfirst.stray_light_fraction) # Make a image of the sky that takes into account the spatially variable pixel scale. Note # that makeSkyImage() takes a bit of time. If you do not care about the variable pixel # scale, you could simply compute an approximate sky level in e-/pix by multiplying # sky_level by wfirst.pixel_scale**2, and add that to final_image. sky_image = final_image.copy() wcs.makeSkyImage(sky_image, sky_level) # This image is in units of e-/pix. Finally we add the expected thermal backgrounds in this # band. These are provided in e-/pix/s, so we have to multiply by the exposure time. sky_image += wfirst.thermal_backgrounds[filter_name] * wfirst.exptime # Adding sky level to the image. final_image += sky_image # Now that all sources of signal (from astronomical objects and background) have been added # to the image, we can start adding noise and detector effects. There is a utility, # galsim.wfirst.allDetectorEffects(), that can apply ALL implemented noise and detector # effects in the proper order. Here we step through the process and explain these in a bit # more detail without using that utility. # First, we include the expected Poisson noise: final_image.addNoise(poisson_noise) # The subsequent steps account for the non-ideality of the detectors. # 1) Reciprocity failure: # Reciprocity, in the context of photography, is the inverse relationship between the # incident flux (I) of a source object and the exposure time (t) required to produce a given # response(p) in the detector, i.e., p = I*t. However, in NIR detectors, this relation does # not hold always. The pixel response to a high flux is larger than its response to a low # flux. This flux-dependent non-linearity is known as 'reciprocity failure', and the # approximate amount of reciprocity failure for the WFIRST detectors is known, so we can # include this detector effect in our images. if diff_mode: # Save the image before applying the transformation to see the difference save_image = final_image.copy() # If we had wanted to, we could have specified a different exposure time than the default # one for WFIRST, but otherwise the following routine does not take any arguments. wfirst.addReciprocityFailure(final_image) logger.debug('Included reciprocity failure in {0}-band image'.format( filter_name)) if diff_mode: # Isolate the changes due to reciprocity failure. diff = final_image - save_image out_filename = os.path.join( outpath, 'demo13_RecipFail_{0}.fits'.format(filter_name)) final_image.write(out_filename) out_filename = os.path.join( outpath, 'demo13_diff_RecipFail_{0}.fits'.format(filter_name)) diff.write(out_filename) # At this point in the image generation process, an integer number of photons gets # detected, hence we have to round the pixel values to integers: final_image.quantize() # 2) Adding dark current to the image: # Even when the detector is unexposed to any radiation, the electron-hole pairs that # are generated within the depletion region due to finite temperature are swept by the # high electric field at the junction of the photodiode. This small reverse bias # leakage current is referred to as 'dark current'. It is specified by the average # number of electrons reaching the detectors per unit time and has an associated # Poisson noise since it is a random event. dark_current = wfirst.dark_current * wfirst.exptime dark_noise = galsim.DeviateNoise( galsim.PoissonDeviate(rng, dark_current)) final_image.addNoise(dark_noise) # NOTE: Sky level and dark current might appear like a constant background that can be # simply subtracted. However, these contribute to the shot noise and matter for the # non-linear effects that follow. Hence, these must be included at this stage of the # image generation process. We subtract these backgrounds in the end. # 3) Applying a quadratic non-linearity: # In order to convert the units from electrons to ADU, we must use the gain factor. The gain # has a weak dependency on the charge present in each pixel. This dependency is accounted # for by changing the pixel values (in electrons) and applying a constant nominal gain # later, which is unity in our demo. # Save the image before applying the transformation to see the difference: if diff_mode: save_image = final_image.copy() # Apply the WFIRST nonlinearity routine, which knows all about the nonlinearity expected in # the WFIRST detectors. wfirst.applyNonlinearity(final_image) # Note that users who wish to apply some other nonlinearity function (perhaps for other NIR # detectors, or for CCDs) can use the more general nonlinearity routine, which uses the # following syntax: # final_image.applyNonlinearity(NLfunc=NLfunc) # with NLfunc being a callable function that specifies how the output image pixel values # should relate to the input ones. logger.debug( 'Applied nonlinearity to {0}-band image'.format(filter_name)) if diff_mode: diff = final_image - save_image out_filename = os.path.join( outpath, 'demo13_NL_{0}.fits'.format(filter_name)) final_image.write(out_filename) out_filename = os.path.join( outpath, 'demo13_diff_NL_{0}.fits'.format(filter_name)) diff.write(out_filename) # Save this image to do the diff after applying IPC. save_image = final_image.copy() # 4) Including Interpixel capacitance: # The voltage read at a given pixel location is influenced by the charges present in the # neighboring pixel locations due to capacitive coupling of sense nodes. This interpixel # capacitance effect is modeled as a linear effect that is described as a convolution of a # 3x3 kernel with the image. The WFIRST IPC routine knows about the kernel already, so the # user does not have to supply it. wfirst.applyIPC(final_image) logger.debug('Applied interpixel capacitance to {0}-band image'.format( filter_name)) if diff_mode: # Isolate the changes due to the interpixel capacitance effect. diff = final_image - save_image out_filename = os.path.join( outpath, 'demo13_IPC_{0}.fits'.format(filter_name)) final_image.write(out_filename) out_filename = os.path.join( outpath, 'demo13_diff_IPC_{0}.fits'.format(filter_name)) diff.write(out_filename) # 5) Adding read noise: # Read noise is the noise due to the on-chip amplifier that converts the charge into an # analog voltage. We already applied the Poisson noise due to the sky level, so read noise # should just be added as Gaussian noise: read_noise = galsim.GaussianNoise(rng, sigma=wfirst.read_noise) final_image.addNoise(read_noise) logger.debug('Added readnoise to {0}-band image'.format(filter_name)) # We divide by the gain to convert from e- to ADU. Currently, the gain value in the WFIRST # module is just set to 1, since we don't know what the exact gain will be, although it is # expected to be approximately 1. Eventually, this may change when the camera is assembled, # and there may be a different value for each SCA. For now, there is just a single number, # which is equal to 1. final_image /= wfirst.gain # Finally, the analog-to-digital converter reads in an integer value. final_image.quantize() # Note that the image type after this step is still a float. If we want to actually # get integer values, we can do new_img = galsim.Image(final_image, dtype=int) # Since many people are used to viewing background-subtracted images, we provide a # version with the background subtracted (also rounding that to an int). tot_sky_image = (sky_image + dark_current) / wfirst.gain tot_sky_image.quantize() final_image -= tot_sky_image logger.debug( 'Subtracted background for {0}-band image'.format(filter_name)) # Write the final image to a file. out_filename = os.path.join(outpath, 'demo13_{0}.fits'.format(filter_name)) final_image.write(out_filename) logger.info('Completed {0}-band image.'.format(filter_name)) logger.info( 'You can display the output in ds9 with a command line that looks something like:' ) logger.info('ds9 -zoom 0.5 -scale limits -500 1000 -rgb ' + '-red output/demo13_H158.fits ' + '-green output/demo13_J129.fits ' + '-blue output/demo13_Y106.fits')
def test_pad_image(): """Test padding an InterpolatedImage with a pad_image.""" import time t1 = time.time() decimal=2 # all are coarse, since there are slight changes from odd/even centering issues. noise_sigma = 1.73 noise_var = noise_sigma**2 orig_seed = 12345 rng = galsim.BaseDeviate(orig_seed) noise = galsim.GaussianNoise(rng, sigma=noise_sigma) # make the original image orig_nx = 64 orig_ny = 64 orig_img = galsim.ImageF(orig_nx, orig_ny, scale=1.) galsim.Exponential(scale_radius=1.7,flux=1000).draw(orig_img) orig_img.addNoise(noise) orig_img.setCenter(0,0) # We'll draw into a larger image for the tests pad_factor = 4 big_nx = pad_factor*orig_nx big_ny = pad_factor*orig_ny big_img = galsim.ImageF(big_nx, big_ny, scale=1.) big_img.setCenter(0,0) # Use a few different kinds of shapes for that padding. for (pad_nx, pad_ny) in [ (160,160), (179,191), (256,256), (305, 307) ]: #print 'pad size = ',pad_nx, pad_ny # make the pad_image pad_img = galsim.ImageF(pad_nx, pad_ny, scale=1.) pad_img.addNoise(noise) pad_img.setCenter(0,0) # make an interpolated image padded with the pad_image, and outside of that int_im = galsim.InterpolatedImage(orig_img, pad_image=pad_img, use_true_center=False) # draw into the larger image int_im.draw(big_img, use_true_center=False) # check that variance is diluted by expected amount # Note -- we don't use np.var, since that computes the variance relative to the # actual mean value. We just want sum(I^2)/Npix relative to the nominal I=0 value. var1 = np.sum(orig_img.array**2) if pad_nx > big_nx and pad_ny > big_ny: var2 = np.sum(pad_img[big_img.bounds].array**2) else: var2 = np.sum(pad_img.array**2) var2 -= np.sum(pad_img[orig_img.bounds].array**2) var_expected = (var1 + var2) / (big_nx*big_ny) big_img.setCenter(0,0) np.testing.assert_almost_equal( np.mean(big_img.array**2), var_expected, decimal=decimal, err_msg='Variance not correct when padding with image') if pad_nx < big_nx and pad_ny < big_ny: # now also pad with noise_pad outside of the pad_image int_im = galsim.InterpolatedImage(orig_img, pad_image=pad_img, noise_pad=noise_var/2, noise_pad_size=max(big_nx,big_ny), rng=rng, use_true_center=False) int_im.draw(big_img, use_true_center=False) var3 = (noise_var/2) * float(big_nx*big_ny - pad_nx*pad_ny) var_expected = (var1 + var2 + var3) / (big_nx*big_ny) np.testing.assert_almost_equal( np.mean(big_img.array**2), var_expected, decimal=decimal, err_msg='Variance not correct after padding with image and extra noise') t2 = time.time() print 'time for %s = %.2f'%(funcname(),t2-t1)
def test_kolmogorov(): """Test the generation of a specific Kolmogorov profile against a known result. """ import math dx = 0.2 test_flux = 1.8 # This savedImg was created from the SBKolmogorov implementation in # commit c8efd74d1930157b1b1ffc0bfcfb5e1bf6fe3201 # It would be nice to get an independent calculation here... #mySBP = galsim.SBKolmogorov(lam_over_r0=1.5, flux=test_flux) #savedImg = galsim.ImageF(128,128) #mySBP.drawImage(image=savedImg, dx=dx, method="sb") #savedImg.write(os.path.join(imgdir, "kolmogorov.fits")) savedImg = galsim.fits.read(os.path.join(imgdir, "kolmogorov.fits")) myImg = galsim.ImageF(savedImg.bounds, scale=dx) myImg.setCenter(0, 0) kolm = galsim.Kolmogorov(lam_over_r0=1.5, flux=test_flux) kolm.drawImage(myImg, method="sb", use_true_center=False) np.testing.assert_array_almost_equal( myImg.array, savedImg.array, 5, err_msg="Using GSObject Kolmogorov disagrees with expected result") # Check with default_params kolm = galsim.Kolmogorov(lam_over_r0=1.5, flux=test_flux, gsparams=default_params) kolm.drawImage(myImg, method="sb", use_true_center=False) np.testing.assert_array_almost_equal( myImg.array, savedImg.array, 5, err_msg= "Using GSObject Kolmogorov with default_params disagrees with expected result" ) kolm = galsim.Kolmogorov(lam_over_r0=1.5, flux=test_flux, gsparams=galsim.GSParams()) kolm.drawImage(myImg, method="sb", use_true_center=False) np.testing.assert_array_almost_equal( myImg.array, savedImg.array, 5, err_msg= "Using GSObject Kolmogorov with GSParams() disagrees with expected result" ) gsp = galsim.GSParams(xvalue_accuracy=1.e-8, kvalue_accuracy=1.e-8) kolm2 = galsim.Kolmogorov(lam_over_r0=1.5, flux=test_flux, gsparams=gsp) assert kolm2 != kolm assert kolm2 == kolm.withGSParams(gsp) check_basic(kolm, "Kolmogorov") # Test photon shooting. do_shoot(kolm, myImg, "Kolmogorov") # Test kvalues do_kvalue(kolm, myImg, "Kolmogorov") # Check picklability do_pickle(kolm, lambda x: x.drawImage(method='no_pixel')) do_pickle(kolm) # Test initialization separately with lam and r0, in various units. Since the above profiles # have lam/r0 = 3./2. in arbitrary units, we will tell it that lam=3.e9 nm and r0=2.0 m, # and use `scale_unit` of galsim.radians. This is rather silly, but it should work. kolm = galsim.Kolmogorov(lam_over_r0=1.5, flux=test_flux) kolm2 = galsim.Kolmogorov(lam=3.e9, r0=2.0, scale_unit=galsim.radians, flux=test_flux) gsobject_compare(kolm, kolm2) # For lam/r0 = 1.5 arcsec, and r0 = 0.2, lam = (1.5/3600/180*pi) * 0.2 * 1.e9 lam = 1.5 * 0.2 / 3600. / 180. * math.pi * 1.e9 print('lam = ', lam) kolm3 = galsim.Kolmogorov(lam=lam, r0=0.2, scale_unit='arcsec', flux=test_flux) gsobject_compare(kolm, kolm3) # arcsec is the default scale_unit, so can leave this off. kolm4 = galsim.Kolmogorov(lam=lam, r0=0.2, flux=test_flux) gsobject_compare(kolm, kolm4) # Test using r0_500 instead r0_500 = 0.2 * (lam / 500)**-1.2 kolm5 = galsim.Kolmogorov(lam=lam, r0_500=r0_500, flux=test_flux) gsobject_compare(kolm, kolm5) # Should raise an exception if >= 2 radius specifications are provided and/or lam and r0 are not # paired together. assert_raises(TypeError, galsim.Kolmogorov, lam_over_r0=3, fwhm=2, half_light_radius=1, lam=3, r0=1) assert_raises(TypeError, galsim.Kolmogorov, fwhm=2, half_light_radius=1, lam=3, r0=1) assert_raises(TypeError, galsim.Kolmogorov, lam_over_r0=3, half_light_radius=1, lam=3, r0=1) assert_raises(TypeError, galsim.Kolmogorov, lam_over_r0=3, fwhm=2, lam=3, r0=1) assert_raises(TypeError, galsim.Kolmogorov, lam_over_r0=3, fwhm=2, half_light_radius=1) assert_raises(TypeError, galsim.Kolmogorov, half_light_radius=1, lam=3, r0=1) assert_raises(TypeError, galsim.Kolmogorov, fwhm=2, lam=3, r0=1) assert_raises(TypeError, galsim.Kolmogorov, fwhm=2, half_light_radius=1) assert_raises(TypeError, galsim.Kolmogorov, lam_over_r0=3, lam=3, r0=1) assert_raises(TypeError, galsim.Kolmogorov, lam_over_r0=3, half_light_radius=1) assert_raises(TypeError, galsim.Kolmogorov, lam_over_r0=3, fwhm=2) assert_raises(TypeError, galsim.Kolmogorov, lam_over_r0=3, lam=3) assert_raises(TypeError, galsim.Kolmogorov, lam_over_r0=3, r0=1) assert_raises(TypeError, galsim.Kolmogorov, fwhm=2, lam=3) assert_raises(TypeError, galsim.Kolmogorov, fwhm=2, r0=1) assert_raises(TypeError, galsim.Kolmogorov, half_light_radius=1, lam=3) assert_raises(TypeError, galsim.Kolmogorov, half_light_radius=1, r0=1) assert_raises(TypeError, galsim.Kolmogorov, lam=3) assert_raises(TypeError, galsim.Kolmogorov, r0=1) assert_raises(TypeError, galsim.Kolmogorov)
def test_conserve_dc(): """Test that the conserve_dc option for Lanczos does so. Note: the idea of conserving flux is a bit of a misnomer. No interpolant does so precisely in general. What we are really testing is that a flat background input image has a relatively flat output image. """ import time t1 = time.time() import numpy im1_size = 40 scale1 = 0.23 init_val = 1. im2_size = 100 scale2 = 0.011 im1 = galsim.ImageF(im1_size, im1_size, scale=scale1, init_value=init_val) # im2 has a much smaller scale, but the same size, so interpolating an "infinite" # constant field. im2 = galsim.ImageF(im2_size, im2_size, scale=scale2) for interp in ['linear', 'cubic', 'quintic']: print 'Testing interpolant ',interp obj = galsim.InterpolatedImage(im1, x_interpolant=interp, normalization='sb') obj.draw(im2, normalization='sb') print 'The maximum error is ',numpy.max(abs(im2.array-init_val)) numpy.testing.assert_array_almost_equal( im2.array,init_val,5, '%s did not preserve a flat input flux using xvals.'%interp) # Convolve with a delta function to force FFT drawing. delta = galsim.Gaussian(sigma=1.e-8) obj2 = galsim.Convolve([obj,delta]) obj2.draw(im2, normalization='sb') print 'The maximum error is ',numpy.max(abs(im2.array-init_val)) numpy.testing.assert_array_almost_equal( im2.array,init_val,5, '%s did not preserve a flat input flux using uvals.'%interp) for n in [3,4,5,6,7,8]: # 8 tests the generic formulae, since not specialized. print 'Testing Lanczos interpolant with n = ',n lan = galsim.Lanczos(n, conserve_dc=True) obj = galsim.InterpolatedImage(im1, x_interpolant=lan, normalization='sb') obj.draw(im2, normalization='sb') print 'The maximum error is ',numpy.max(abs(im2.array-init_val)) numpy.testing.assert_array_almost_equal( im2.array,init_val,5, 'Lanczos %d did not preserve a flat input flux using xvals.'%n) # Convolve with a delta function to force FFT drawing. delta = galsim.Gaussian(sigma=1.e-8) obj2 = galsim.Convolve([obj,delta]) obj2.draw(im2, normalization='sb') print 'The maximum error is ',numpy.max(abs(im2.array-init_val)) numpy.testing.assert_array_almost_equal( im2.array,init_val,5, 'Lanczos %d did not preserve a flat input flux using uvals.'%n) t2 = time.time() print 'time for %s = %.2f'%(funcname(),t2-t1)
def test_fourier_sqrt(): """Test that the FourierSqrt operator is the inverse of auto-convolution. """ dx = 0.4 myImg1 = galsim.ImageF(80, 80, scale=dx) myImg1.setCenter(0, 0) myImg2 = galsim.ImageF(80, 80, scale=dx) myImg2.setCenter(0, 0) # Test trivial case, where we could (but don't) analytically collapse the # chain of profiles by recognizing that FourierSqrt is the inverse of # AutoConvolve. psf = galsim.Moffat(beta=3.8, fwhm=1.3, flux=5) psf.drawImage(myImg1, method='no_pixel') sqrt1 = galsim.FourierSqrt(psf) psf2 = galsim.AutoConvolve(sqrt1) np.testing.assert_almost_equal(psf.stepk, psf2.stepk) psf2.drawImage(myImg2, method='no_pixel') printval(myImg1, myImg2) np.testing.assert_array_almost_equal( myImg1.array, myImg2.array, 4, err_msg="Moffat sqrt convolved with self disagrees with original") check_basic(sqrt1, "FourierSqrt", do_x=False) # Test non-trivial case where we compare (in Fourier space) sqrt(a*a + b*b + 2*a*b) against (a + b) a = galsim.Moffat(beta=3.8, fwhm=1.3, flux=5) a.shift(dx=0.5, dy=-0.3) # need nonzero centroid to test b = galsim.Moffat(beta=2.5, fwhm=1.6, flux=3) check = galsim.Sum([a, b]) sqrt = galsim.FourierSqrt( galsim.Sum([ galsim.AutoConvolve(a), galsim.AutoConvolve(b), 2 * galsim.Convolve([a, b]) ])) np.testing.assert_almost_equal(check.stepk, sqrt.stepk) check.drawImage(myImg1, method='no_pixel') sqrt.drawImage(myImg2, method='no_pixel') np.testing.assert_almost_equal(check.centroid.x, sqrt.centroid.x) np.testing.assert_almost_equal(check.centroid.y, sqrt.centroid.y) np.testing.assert_almost_equal(check.flux, sqrt.flux) np.testing.assert_almost_equal(check.xValue(check.centroid), check.max_sb) print('check.max_sb = ', check.max_sb) print('sqrt.max_sb = ', sqrt.max_sb) # This isn't super accurate... np.testing.assert_allclose(check.max_sb, sqrt.max_sb, rtol=0.1) printval(myImg1, myImg2) np.testing.assert_array_almost_equal( myImg1.array, myImg2.array, 4, err_msg="Fourier square root of expanded square disagrees with original" ) # Check picklability do_pickle(sqrt1, lambda x: x.drawImage(method='no_pixel')) do_pickle(sqrt1) # Should raise an exception for invalid arguments assert_raises(TypeError, galsim.FourierSqrt) assert_raises(TypeError, galsim.FourierSqrt, myImg1) assert_raises(TypeError, galsim.FourierSqrt, [psf]) assert_raises(TypeError, galsim.FourierSqrt, psf, psf) assert_raises(TypeError, galsim.FourierSqrt, psf, real_space=False) assert_raises(TypeError, galsim.FourierSqrtProfile) assert_raises(TypeError, galsim.FourierSqrtProfile, myImg1) assert_raises(TypeError, galsim.FourierSqrtProfile, [psf]) assert_raises(TypeError, galsim.FourierSqrtProfile, psf, psf) assert_raises(TypeError, galsim.FourierSqrtProfile, psf, real_space=False) assert_raises(NotImplementedError, sqrt1.xValue, galsim.PositionD(0, 0)) assert_raises(NotImplementedError, sqrt1.drawReal, myImg1) assert_raises(NotImplementedError, sqrt1.shoot, 1)
def build_file(seed, file_name, mass, nobj): """A function that does all the work to build a single file. Returns the total time taken. """ t1 = time.time() # Build the image onto which we will draw the galaxies. full_image = galsim.ImageF(image_size, image_size) # The "true" center of the image is allowed to be halfway between two pixels, as is the # case for even-sized images. full_image.bounds.center() is an integer position, # which would be 1/2 pixel up and to the right of the true center in this case. im_center = full_image.bounds.trueCenter() # For the WCS, this time we use UVFunction, which lets you define arbitrary u(x,y) # and v(x,y) functions. We use a simple cubic radial function to create a # pincushion distortion. This is a typical kind of telescope distortion, although # we exaggerate the magnitude of the effect to make it more apparent. # The pixel size in the center of the image is 0.05, but near the corners (r=362), # the pixel size is approximately 0.075, which is much more distortion than is # normally present in typical telescopes. But it makes the effect of the variable # pixel area obvious when you look at the weight image in the output files. ufunc1 = lambda x, y: 0.05 * x * (1. + 2.e-6 * (x**2 + y**2)) vfunc1 = lambda x, y: 0.05 * y * (1. + 2.e-6 * (x**2 + y**2)) # It's not required to provide the inverse functions. However, if we don't, then # you will only be able to do toWorld operations, not the inverse toImage. # The inverse function does not have to be exact either. For example, you could provide # a function that does some kind of iterative solution to whatever accuracy you care # about. But in this case, we can do the exact inverse. # # Let w = sqrt(u**2 + v**2) and r = sqrt(x**2 + y**2). Then the solutions are: # x = (u/w) r and y = (u/w) r, and we use Cardano's method to solve for r given w: # See http://en.wikipedia.org/wiki/Cubic_function#Cardano.27s_method # # w = 0.05 r + 2.e-6 * 0.05 * r**3 # r = 100 * ( ( 5 sqrt(w**2 + 5.e3/27) + 5 w )**(1./3.) - # - ( 5 sqrt(w**2 + 5.e3/27) - 5 w )**(1./3.) ) def xfunc1(u, v): import math wsq = u * u + v * v if wsq == 0.: return 0. else: w = math.sqrt(wsq) temp = 5. * math.sqrt(wsq + 5.e3 / 27) r = 100. * ((temp + 5 * w)**(1. / 3.) - (temp - 5 * w)**(1. / 3)) return u * r / w def yfunc1(u, v): import math wsq = u * u + v * v if wsq == 0.: return 0. else: w = math.sqrt(wsq) temp = 5. * math.sqrt(wsq + 5.e3 / 27) r = 100. * ((temp + 5 * w)**(1. / 3.) - (temp - 5 * w)**(1. / 3)) return v * r / w # You could pass the above functions to UVFunction, and normally we would do that. # The only down side to doing so is that the specification of the WCS in the FITS # file is rather ugly. GalSim is able to turn the python byte code into strings, # but they are basically a really ugly mess of random-looking characters. GalSim # will be able to read it back in, but human readers will have no idea what WCS # function was used. To see what they look like, uncomment this line and comment # out the later wcs line. #wcs = galsim.UVFunction(ufunc1, vfunc1, xfunc1, yfunc1, origin=im_center) # If you provide the functions as strings, then those strings will be preserved # in the FITS header in a form that is more legible to human readers. # It also has the extra benefit of matching the output from demo9.yaml, which we # always try to do. The config file has no choice but to specify the functions # as strings. ufunc = '0.05 * x * (1. + 2.e-6 * (x**2 + y**2))' vfunc = '0.05 * y * (1. + 2.e-6 * (x**2 + y**2))' xfunc = ( '( lambda w: ( 0 if w==0 else ' + '100.*u/w*(( 5*(w**2 + 5.e3/27.)**0.5 + 5*w )**(1./3.) - ' + '( 5*(w**2 + 5.e3/27.)**0.5 - 5*w )**(1./3.))))( (u**2+v**2)**0.5 )' ) yfunc = ( '( lambda w: ( 0 if w==0 else ' + '100.*v/w*(( 5*(w**2 + 5.e3/27.)**0.5 + 5*w )**(1./3.) - ' + '( 5*(w**2 + 5.e3/27.)**0.5 - 5*w )**(1./3.))))( (u**2+v**2)**0.5 )' ) # The origin parameter defines where on the image should be considered (x,y) = (0,0) # in the WCS functions. wcs = galsim.UVFunction(ufunc, vfunc, xfunc, yfunc, origin=im_center) # Assign this wcs to full_image full_image.wcs = wcs # The weight image will hold the inverse variance for each pixel. # We can set the wcs directly on construction with the wcs parameter. weight_image = galsim.ImageF(image_size, image_size, wcs=wcs) # It is common for astrometric images to also have a bad pixel mask. We don't have any # defect simulation currently, so our bad pixel masks are currently all zeros. # But someday, we plan to add defect functionality to GalSim, at which point, we'll # be able to mark those defects on a bad pixel mask. # Note: the S in ImageS means to use "short int" for the data type. # This is a typical choice for a bad pixel image. badpix_image = galsim.ImageS(image_size, image_size, wcs=wcs) # We also draw a PSF image at the location of every galaxy. This isn't normally done, # and since some of the PSFs overlap, it's not necessarily so useful to have this kind # of image. But in this case, it's fun to look at the psf image, especially with # something like log scaling in ds9 to see how crazy an aberrated OpticalPSF with # struts can look when there is no atmospheric component to blur it out. psf_image = galsim.ImageF(image_size, image_size, wcs=wcs) # Setup the NFWHalo stuff: nfw = galsim.NFWHalo(mass=mass, conc=nfw_conc, redshift=nfw_z_halo, omega_m=omega_m, omega_lam=omega_lam) # Note: the last two are optional. If they are omitted, then (omega_m=0.3, omega_lam=0.7) # are actually the defaults. If you only specify one of them, the other is set so that # the total is 1. But you can define both values so that the total is not 1 if you want. # Radiation is assumed to be zero and dark energy equation of state w = -1. # If you want to include either radiation or more complicated dark energy models, # you can define your own cosmology class that defines the functions a(z), E(a), and # Da(z_source, z_lens). Then you can pass this to NFWHalo as a `cosmo` parameter. # Make the PSF profile outside the loop to minimize the (significant) OpticalPSF # construction overhead. psf = galsim.OpticalPSF(lam_over_diam=psf_lam_over_D, obscuration=psf_obsc, nstruts=psf_nstruts, strut_thick=psf_strut_thick, strut_angle=psf_strut_angle, defocus=psf_defocus, astig1=psf_astig1, astig2=psf_astig2, coma1=psf_coma1, coma2=psf_coma2, trefoil1=psf_trefoil1, trefoil2=psf_trefoil2) for k in range(nobj): # Initialize the random number generator we will be using for this object: rng = galsim.UniformDeviate(seed + k) # Determine where this object is going to go. # We choose points randomly within a donut centered at the center of the main image # in order to avoid placing galaxies too close to the halo center where the lensing # is not weak. We use an inner radius of 3 arcsec and an outer radius of 12 arcsec, # which takes us essentially to the edge of the image. radius = 12 inner_radius = 3 max_rsq = radius**2 min_rsq = inner_radius**2 while True: # (This is essentially a do..while loop.) x = (2. * rng() - 1) * radius y = (2. * rng() - 1) * radius rsq = x**2 + y**2 if rsq >= min_rsq and rsq <= max_rsq: break pos = galsim.PositionD(x, y) # We also need the position in pixels to determine where to place the postage # stamp on the full image. image_pos = wcs.toImage(pos) # For even-sized postage stamps, the nominal center (returned by stamp.bounds.center()) # cannot be at the true center (returned by stamp.bounds.trueCenter()) of the postage # stamp, since the nominal center values have to be integers. Thus, the nominal center # is 1/2 pixel up and to the right of the true center. # If we used odd-sized postage stamps, we wouldn't need to do this. x_nominal = image_pos.x + 0.5 y_nominal = image_pos.y + 0.5 # Get the integer values of these which will be the actual nominal center of the # postage stamp image. ix_nominal = int(math.floor(x_nominal + 0.5)) iy_nominal = int(math.floor(y_nominal + 0.5)) # The remainder will be accounted for in an offset when we draw. dx = x_nominal - ix_nominal dy = y_nominal - iy_nominal offset = galsim.PositionD(dx, dy) # Determine the random values for the galaxy: flux = rng() * (gal_flux_max - gal_flux_min) + gal_flux_min hlr = rng() * (gal_hlr_max - gal_hlr_min) + gal_hlr_min gd = galsim.GaussianDeviate(rng, sigma=gal_eta_rms) eta1 = gd( ) # Unlike g or e, large values of eta are valid, so no need to cutoff. eta2 = gd() # Make the galaxy profile with these values: gal = galsim.Exponential(half_light_radius=hlr, flux=flux) gal = gal.shear(eta1=eta1, eta2=eta2) # Now apply the appropriate lensing effects for this position from # the NFW halo mass. try: g1, g2 = nfw.getShear(pos, nfw_z_source) nfw_shear = galsim.Shear(g1=g1, g2=g2) except: # This shouldn't happen, since we exclude the inner 10 arcsec, but it's a # good idea to use the try/except block here anyway. import warnings warnings.warn( "Warning: NFWHalo shear is invalid -- probably strong lensing! " + "Using shear = 0.") nfw_shear = galsim.Shear(g1=0, g2=0) nfw_mu = nfw.getMagnification(pos, nfw_z_source) if nfw_mu < 0: import warnings warnings.warn( "Warning: mu < 0 means strong lensing! Using mu=25.") nfw_mu = 25 elif nfw_mu > 25: import warnings warnings.warn( "Warning: mu > 25 means strong lensing! Using mu=25.") nfw_mu = 25 # Calculate the total shear to apply # Since shear addition is not commutative, it is worth pointing out that # the order is in the sense that the second shear is applied first, and then # the first shear. i.e. The field shear is taken to be behind the cluster. # Kind of a cosmic shear contribution between the source and the cluster. # However, this is not quite the same thing as doing: # gal.shear(field_shear).shear(nfw_shear) # since the shear addition ignores the rotation that would occur when doing the # above lines. This is normally ok, because the rotation is not observable, but # it is worth keeping in mind. total_shear = nfw_shear + field_shear # Apply the magnification and shear to the galaxy gal = gal.magnify(nfw_mu) gal = gal.shear(total_shear) # Build the final object final = galsim.Convolve([psf, gal]) # Draw the stamp image # To draw the image at a position other than the center of the image, you can # use the offset parameter, which applies an offset in pixels relative to the # center of the image. # We also need to provide the local wcs at the current position. local_wcs = wcs.local(image_pos) stamp = final.drawImage(wcs=local_wcs, offset=offset) # Recenter the stamp at the desired position: stamp.setCenter(ix_nominal, iy_nominal) # Find overlapping bounds bounds = stamp.bounds & full_image.bounds full_image[bounds] += stamp[bounds] # Also draw the PSF psf_stamp = galsim.ImageF( stamp.bounds) # Use same bounds as galaxy stamp psf.drawImage(psf_stamp, wcs=local_wcs, offset=offset) psf_image[bounds] += psf_stamp[bounds] # Add Poisson noise to the full image # Note: The normal calculation of Poission noise isn't quite correct right now. # The pixel area is variable, which means the amount of sky flux that enters each # pixel is also variable. The wcs classes have a function `makeSkyImage` which # will fill an image with the correct amount of sky flux given the sky level # in units of ADU/arcsec^2. We use the weight image as our work space for this. wcs.makeSkyImage(weight_image, sky_level) # Add this to the current full_image (temporarily). full_image += weight_image # Add Poisson noise, given the current full_image. # Going to the next seed isn't really required, but it matches the behavior of the # config parser, so doing this will result in identical output files. # If you didn't care about that, you could instead construct this as a continuation # of the last RNG from the above loop rng = galsim.BaseDeviate(seed + nobj) full_image.addNoise(galsim.PoissonNoise(rng)) # Subtract the sky back off. full_image -= weight_image # The weight image is nominally the inverse variance of the pixel noise. However, it is # common to exclude the Poisson noise from the objects themselves and only include the # noise from the sky photons. The variance of the noise is just the sky level, which is # what is currently in the weight_image. (If we wanted to include the variance from the # objects too, then we could use the full_image before we added the PoissonNoise to it.) # So all we need to do now is to invert the values in weight_image. weight_image.invertSelf() # Write the file to disk: galsim.fits.writeMulti( [full_image, badpix_image, weight_image, psf_image], file_name) t2 = time.time() return t2 - t1
def main(argv): # Where to find and output data path, filename = os.path.split(__file__) datapath = os.path.abspath(os.path.join(path, "data/")) outpath = os.path.abspath(os.path.join(path, "output/")) if not os.path.exists(outpath): print 'Creating', outpath os.makedirs(outpath) # In non-script code, use getLogger(__name__) at module scope instead. logging.basicConfig(format="%(message)s", level=logging.INFO, stream=sys.stdout) logger = logging.getLogger("demo12") # initialize (pseudo-)random number generator random_seed = 1234567 rng = galsim.BaseDeviate(random_seed) # read in SEDs SED_names = ['CWW_E_ext', 'CWW_Sbc_ext', 'CWW_Scd_ext', 'CWW_Im_ext'] SEDs = {} for SED_name in SED_names: SED_filename = os.path.join(datapath, '{}.sed'.format(SED_name)) # Here we create some galsim.SED objects to hold star or galaxy spectra. The most # convenient way to create realistic spectra is to read them in from a two-column ASCII # file, where the first column is wavelength and the second column is flux. Wavelengths in # the example SED files are in Angstroms, flux in flambda. The default wavelength type for # galsim.SED is nanometers, however, so we need to override by specifying # `wave_type = 'Ang'`. SED = galsim.SED(SED_filename, wave_type='Ang') # The normalization of SEDs affects how many photons are eventually drawn into an image. # One way to control this normalization is to specify the flux density in photons per nm # at a particular wavelength. For example, here we normalize such that the photon density # is 1 photon per nm at 500 nm. SEDs[SED_name] = SED.withFluxDensity(target_flux_density=1.0, wavelength=500) logger.debug('Successfully read in SEDs') # read in the LSST filters filter_names = 'ugrizy' filters = {} for filter_name in filter_names: filter_filename = os.path.join(datapath, 'LSST_{}.dat'.format(filter_name)) # Here we create some galsim.Bandpass objects to represent the filters we're observing # through. These include the entire imaging system throughput including the atmosphere, # reflective and refractive optics, filters, and the CCD quantum efficiency. These are # also conveniently read in from two-column ASCII files where the first column is # wavelength and the second column is dimensionless flux. The example filter files have # units of nanometers and dimensionless throughput, which is exactly what galsim.Bandpass # expects, so we just specify the filename. filters[filter_name] = galsim.Bandpass(filter_filename) # For speed, we can thin out the wavelength sampling of the filter a bit. # In the following line, `rel_err` specifies the relative error when integrating over just # the filter (however, this is not necessarily the relative error when integrating over the # filter times an SED) filters[filter_name] = filters[filter_name].thin(rel_err=1e-4) logger.debug('Read in filters') pixel_scale = 0.2 # arcseconds #----------------------------------------------------------------------------------------------- # Part B: chromatic bulge+disk galaxy logger.info('') logger.info('Starting part B: chromatic bulge+disk galaxy') redshift = 0.8 # make a bulge ... mono_bulge = galsim.DeVaucouleurs(half_light_radius=0.5) bulge_SED = SEDs['CWW_E_ext'].atRedshift(redshift) # The `*` operator can be used as a shortcut for creating a chromatic version of a GSObject: bulge = mono_bulge * bulge_SED bulge = bulge.shear(g1=0.12, g2=0.07) logger.debug('Created bulge component') # ... and a disk ... mono_disk = galsim.Exponential(half_light_radius=2.0) disk_SED = SEDs['CWW_Im_ext'].atRedshift(redshift) disk = mono_disk * disk_SED disk = disk.shear(g1=0.4, g2=0.2) logger.debug('Created disk component') # ... and then combine them. bdgal = 1.1 * ( 0.8 * bulge + 4 * disk ) # you can add and multiply ChromaticObjects just like GSObjects # convolve with PSF to make final profile #PSF = galsim.Moffat(fwhm=0.6, beta=2.5) psf_sigma = 1.5 * pixel_scale # in arcsec PSF = galsim.Gaussian(flux=1., sigma=psf_sigma) bdfinal = galsim.Convolve([bdgal, PSF]) # Note that at this stage, our galaxy is chromatic but our PSF is still achromatic. Part C) # below will dive into chromatic PSFs. logger.debug('Created bulge+disk galaxy final profile') # draw profile through LSST filters gaussian_noise = galsim.GaussianNoise(rng, sigma=0.02) for filter_name, filter_ in filters.iteritems(): img = galsim.ImageF(64, 64, scale=pixel_scale) bdfinal.drawImage(filter_, image=img) logger.debug('Created {}-band image'.format(filter_name)) out_filename = os.path.join(outpath, 'demo12b_{}.fits'.format(filter_name)) nepochs = 3 images = [] for i in range(nepochs): newImg = img.copy() newImg.addNoise(gaussian_noise) images.append(newImg) galsim.fits.writeCube(images, out_filename) logger.debug('Wrote {}-band images to disk'.format(filter_name)) logger.info('Added flux for {}-band image: {}'.format( filter_name, img.added_flux)) logger.info( 'You can display the output in ds9 with a command line that looks something like:' ) logger.info( 'ds9 -rgb -blue -scale limits -0.2 0.8 output/demo12b_r.fits -green -scale limits' + ' -0.25 1.0 output/demo12b_i.fits -red -scale limits -0.25 1.0 output/demo12b_z.fits' + ' -zoom 2 &')