def FlattenNoiseVariance(config, full_image, stamps, current_vars, logger): """This is a helper function to bring the noise level up to a constant value across the image. If some of the galaxies are RealGalaxy objects and noise whitening (or symmetrizing) is turned on, then there will already be some noise in the stamps that get built. This function goes through and figures out what the maximum current variance is anywhere in the full image and adds noise to the other pixels to bring everything up to that level. @param config The configuration dict. @param full_image The full image onto which the noise should be added. @param stamps A list of the individual postage stamps. @param current_vars A list of the current variance in each postage stamps. @param logger If given, a logger object to log progress. @returns the final variance in the image """ rng = config['rng'] nobjects = len(stamps) max_current_var = max(current_vars) if max_current_var > 0: if logger: logger.debug('image %d: maximum noise varance in any stamp is %f', config['image_num'], max_current_var) import numpy # Then there was whitening applied in the individual stamps. # But there could be a different variance in each postage stamp, so the first # thing we need to do is bring everything up to a common level. noise_image = galsim.ImageF(full_image.bounds) for k in range(nobjects): if stamps[k] is None: continue b = stamps[k].bounds & full_image.bounds if b.isDefined(): noise_image[b] += current_vars[k] # Update this, since overlapping postage stamps may have led to a larger # value in some pixels. max_current_var = numpy.max(noise_image.array) if logger: logger.debug('image %d: maximum noise varance in any pixel is %f', config['image_num'], max_current_var) # Figure out how much noise we need to add to each pixel. noise_image = max_current_var - noise_image # Add it. full_image.addNoise(galsim.VariableGaussianNoise(rng, noise_image)) # Now max_current_var is how much noise is in each pixel. return max_current_var
def addnoise(self, stamp, ivarstamp, gain=4.0): # Invert the inverse variance stamp, cleaning up zeros. varstamp = ivarstamp.copy() varstamp.invertSelf() # [ADU^2] if np.min(varstamp.array) < 0: print('Negative var!') print(np.min(varstamp.array)) sys.exit(1) #medvar = np.median(varstamp.array[varstamp.array>0]) #print('Median variance ', medvar) #varstamp.array[varstamp.array<=0] = medvar # Convert to electrons. stamp *= gain # [electron] varstamp *= (gain**2) # [electron^2] firstvarstamp = varstamp + stamp # Add Poisson noise #plt.imshow(varstamp.array) ; plt.show() #print('Turning off adding noise!') plt.imshow(stamp.array) plt.show() #stamp.addNoise(galsim.VariableGaussianNoise(galsim.BaseDeviate(),varstamp)) stamp.addNoise( galsim.VariableGaussianNoise(galsim.BaseDeviate(), firstvarstamp)) plt.imshow(stamp.array) plt.show() varstamp += stamp #plt.imshow(varstamp.array) ; plt.show() # Convert back to ADU stamp /= gain # [ADU] varstamp /= (gain**2) # [ADU^2] ivarstamp = varstamp.copy() ivarstamp.invertSelf() # [1/ADU^2] return stamp, ivarstamp
def addnoise(self, stamp, ivarstamp): """Add noise to the object postage stamp. Remember that STAMP and IVARSTAMP are in units of nanomaggies and 1/nanomaggies**2, respectively. """ varstamp = ivarstamp.copy() varstamp.invertSelf() if np.min(varstamp.array) < 0: print(np.min(varstamp.array)) #sys.exit(1) # Add the variance of the object to the variance image (in electrons). stamp *= self.nano2e # [electron] #stamp.array = np.abs(stamp.array) st = np.abs(stamp.array) stamp = galsim.Image(st) varstamp *= self.nano2e**2 # [electron^2] firstvarstamp = varstamp + stamp # Add Poisson noise stamp.addNoise( galsim.VariableGaussianNoise(galsim.BaseDeviate(), firstvarstamp)) # ensure the Poisson variance from the object is >0 (see Galsim.demo13) objvar = galsim.Image(np.sqrt(stamp.array**2), scale=stamp.scale) objvar.setOrigin(galsim.PositionI(stamp.xmin, stamp.ymin)) varstamp += objvar # Convert back to [nanomaggies] stamp /= self.nano2e varstamp /= self.nano2e**2 ivarstamp = varstamp.copy() ivarstamp.invertSelf() return stamp, ivarstamp
def main(argv): """ Make images using model PSFs and galaxy cluster shear: - The galaxies come from COSMOSCatalog, which can produce either RealGalaxy profiles (like in demo10) and parametric fits to those profiles. We choose 40% 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. """ global logger logging.basicConfig(format="%(message)s", level=logging.INFO, stream=sys.stdout) logger = logging.getLogger("mock_superbit_data") # Define some parameters we'll use below. # Normally these would be read in from some parameter file. global pixel_scale pixel_scale = 0.206 # arcsec/pixel global image_xsize image_xsize = 6665 # size of image in pixels global image_ysize image_ysize = 4453 # size of image in pixels global image_xsize_arcsec image_xsize_arcsec = image_xsize * pixel_scale # size of big image in each dimension (arcsec) global image_ysize_arcsec image_ysize_arcsec = image_ysize * pixel_scale # size of big image in each dimension (arcsec) global center_ra center_ra = 19.3 * galsim.hours # The RA, Dec of the center of the image on the sky global center_dec center_dec = -33.1 * galsim.degrees global exp_time exp_time = 300 global sky_bkg # mean sky background from AG's paper sky_bkg = 0.32 # ADU / s / pix global sky_sigma # standard deviation of sky background sky_sigma = 0.16 # ADU / s / pix global nobj nobj = 22 # number of galaxies in entire field global nstars nstars = 300 # number of stars in the entire field global flux_scaling global tel_diam tel_diam = 0.5 global lam lam = 625 # Central wavelength for Airy disk global optics psf_path = '/Users/jemcclea/Research/GalSim/examples/data/fpsc_flight_jitter_psf_oversampled_fixed_10x' global optics # will store the Zernicke component of the PSF global nfw # will store the NFWHalo information global cosmos_cat # will store the COSMOS catalog from which we draw objects global example_cat # also a COSMOS catalog which will contain cluster galaxies # Set up the NFWHalo: mass = 5E14 # Cluster mass (Msol/h) nfw_conc = 4 # Concentration parameter = virial radius / NFW scale radius nfw_z_halo = 0.17 # redshift of the halo omega_m = 0.3 # Omega matter for the background cosmology. omega_lam = 0.7 # Omega lambda for the background cosmology. nfw = galsim.NFWHalo(mass=mass, conc=nfw_conc, redshift=nfw_z_halo, omega_m=omega_m, omega_lam=omega_lam) logger.info('Set up NFW halo for lensing') # Read in galaxy catalog cat_file_name = 'real_galaxy_catalog_25.2.fits' dir = 'data/COSMOS_25.2_training_sample/' cosmos_cat = galsim.COSMOSCatalog(cat_file_name, dir=dir) logger.info('Read in %d galaxies from catalog', cosmos_cat.nobjects) # Also read in example catalog example_cat_file_name = 'data/real_galaxy_catalog_23.5_example.fits' example_cat = galsim.COSMOSCatalog(example_cat_file_name) # The catalog returns objects that are appropriate for HST in 1 second exposures. So for our # telescope we scale up by the relative area, exposure time and pixel scale hst_eff_area = 2.4**2 * (1. - 0.33**2) sbit_eff_area = tel_diam**2 * (1. - 0.380**2) flux_scaling = (sbit_eff_area / hst_eff_area) * exp_time * (pixel_scale / .05)**2 ### Now create PSF. First, define Zernicke polynomial component lam_over_diam = lam * 1.e-9 / tel_diam # radians lam_over_diam *= 206265 # arcsec aberrations = [0.0] * 12 # Set the initial size. aberrations[4] = -0.00725859 # Noll index 4 = Defocus aberrations[5:7] = [0.0, -0.00] # Noll index 5,6 = Astigmatism aberrations[7:9] = [0.07, 0.00] # Noll index 7,8 = Coma aberrations[11] = 0.00133254 # Noll index 11 = Spherical logger.info('Calculated lambda over diam = %f arcsec', lam_over_diam) optics = galsim.OpticalPSF(lam_over_diam, obscuration=0.380, aberrations=aberrations) logger.info('Made telescope PSF profile') ### ### LOOP OVER PSFs TO MAKE GROUPS OF IMAGES ### WITHIN EACH PSF, ITERATE 5 TIMES TO MAKE 5 SEPARATE IMAGES ### all_psfs = glob.glob(psf_path + "/*247530*.psf") # this is 121s logger.info('Beginning loop over jitter/optical psfs') for psf_filen in all_psfs: logger.info('Beginning PSF %s...' % psf_filen) for i in numpy.arange(1, 2): logger.info('Beginning loop %d' % i) random_seed = 23058923781 rng = galsim.BaseDeviate(random_seed) # This is specific to Javier mock PSFs try: root = psf_filen.split('data/')[1].split('/')[0] timescale = psf_filen.split('_10x/')[1].split('.')[0] outname = ''.join([ 'mock_superbit_', root, timescale, str(i).zfill(3), '.fits' ]) truth_file_name = ''.join([ './output/truth_', root, timescale, str(i).zfill(3), '.dat' ]) file_name = os.path.join('output', outname) except: pdb.set_trace() # Setting up a truth catalog names = [ 'gal_num', 'x_image', 'y_image', 'ra', 'dec', 'g1_meas', 'g2_meas', 'nfw_mu', 'redshift', 'flux' ] types = [ int, float, float, float, float, float, float, float, float, float ] truth_catalog = galsim.OutputCatalog(names, types) # Set up the image: full_image = galsim.ImageF(image_xsize, image_ysize) sky_level = exp_time * sky_bkg full_image.fill(sky_level) full_image.setOrigin(0, 0) # We keep track of how much noise is already in the image from the RealGalaxies. noise_image = galsim.ImageF(image_xsize, image_ysize) 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 = numpy.cos(theta) * pixel_scale dudy = -numpy.sin(theta) * pixel_scale dvdx = numpy.sin(theta) * pixel_scale dvdy = numpy.cos(theta) * pixel_scale image_center = full_image.true_center affine = galsim.AffineTransform(dudx, dudy, dvdx, dvdy, origin=full_image.true_center) sky_center = galsim.CelestialCoord(ra=center_ra, dec=center_dec) wcs = galsim.TanWCS(affine, sky_center, units=galsim.arcsec) full_image.wcs = wcs # Now let's read in the PSFEx PSF model. We read the image directly into an # InterpolatedImage GSObject, so we can manipulate it as needed psf_wcs = wcs psf_file = os.path.join(psf_path, psf_filen) psf = galsim.des.DES_PSFEx(psf_file, wcs=psf_wcs) logger.info('Constructed PSF object from PSFEx file') # Loop over galaxy 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) try: # make single galaxy object stamp, truth = make_a_galaxy(ud=ud, wcs=wcs, psf=psf, affine=affine) # Find the overlapping bounds: bounds = stamp.bounds & full_image.bounds # We need to keep track of how much variance we have currently in the image, so when # we add more noise, we can omit what is already there. noise_image[bounds] += truth.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 positioned relative to center t=%f s', k, tot_time) this_flux = numpy.sum(stamp.array) row = [ k, truth.x, truth.y, truth.ra, truth.dec, truth.g1, truth.g2, truth.mu, truth.z, this_flux ] truth_catalog.addRow(row) except: logger.info('Galaxy %d has failed, skipping...', k) pdb.set_trace() ###### Inject cluster galaxy objects: random_seed = 892465352 for k in range(50): time1 = time.time() # The usual random number generator using a different seed for each galaxy. ud = galsim.UniformDeviate(random_seed + k + 1) try: # make single galaxy object cluster_stamp, truth = make_cluster_galaxy(ud=ud, wcs=wcs, psf=psf, affine=affine) # Find the overlapping bounds: bounds = cluster_stamp.bounds & full_image.bounds # We need to keep track of how much variance we have currently in the image, so when # we add more noise, we can omit what is already there. noise_image[bounds] += truth.variance # Finally, add the stamp to the full image. full_image[bounds] += cluster_stamp[bounds] time2 = time.time() tot_time = time2 - time1 logger.info( 'Cluster galaxy %d positioned relative to center t=%f s', k, tot_time) this_flux = numpy.sum(stamp.array) row = [ k, truth.x, truth.y, truth.ra, truth.dec, truth.g1, truth.g2, truth.mu, truth.z, this_flux ] truth_catalog.addRow(row) except: logger.info('Cluster galaxy %d has failed, skipping...', k) pdb.set_trace() #### ### Now repeat process for stars! #### random_seed_stars = 2308173501873 for k in range(nstars): time1 = time.time() ud = galsim.UniformDeviate(random_seed_stars + k + 1) star_stamp, truth = make_a_star(ud=ud, wcs=wcs, psf=psf, affine=affine) bounds = star_stamp.bounds & full_image.bounds # Add the stamp to the full image. try: full_image[bounds] += star_stamp[bounds] time2 = time.time() tot_time = time2 - time1 logger.info( 'Star %d: positioned relative to center, t=%f s', k, tot_time) this_flux = numpy.sum(star_stamp.array) row = [ k, truth.x, truth.y, truth.ra, truth.dec, truth.g1, truth.g2, truth.mu, truth.z, this_flux ] truth_catalog.addRow(row) except: logger.info('Star %d has failed, skipping...', k) pass # If real-type COSMOS galaxies are used, the noise across the image won't be uniform. Since this code is # using parametric-type galaxies, the following section is commented out. # # The first thing to do is to make the Gaussian noise uniform across the whole 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. sky_sigma -= numpy.sqrt(max_current_variance) # Regardless of galaxy type, add Gaussian noise with this variance to the final image. this_noise_sigma = sky_sigma * exp_time noise = galsim.GaussianNoise(rng, sigma=this_noise_sigma) full_image.addNoise(noise) logger.debug('Added noise to final output image') full_image.write(file_name) # Write truth catalog to file. truth_catalog.write(truth_file_name) logger.info('Wrote image to %r', file_name) logger.info(' ') logger.info('completed run %d for psf %s', i, psf_filen) i = i + 1 logger.info(' ') logger.info(' ') logger.info('completed all images') logger.info(' ')
def main(argv): """ Make images using constant PSF and variable shear: - The main image is 2048 x 2048 pixels. - Pixel scale is 0.2 arcsec/pixel, hence the image is about 0.11 degrees on a side. - Applied shear is from a cosmological power spectrum read in from file. - The PSF is a real one from SDSS, and corresponds to a convolution of atmospheric PSF, optical PSF, and pixel response, which has been sampled at pixel centers. We used a PSF from SDSS in order to have a PSF profile that could correspond to what you see with a real telescope. However, in order that the galaxy resolution not be too poor, we tell GalSim that the pixel scale for that PSF image is 0.2" rather than 0.396". We are simultaneously lying about the intrinsic size of the PSF and about the pixel scale when we do this. - The galaxies come from COSMOSCatalog, which can produce either RealGalaxy profiles (like in demo10) and parametric fits to those profiles. We choose 30% of the galaxies to use the images, and the other 60% to use the parametric fits - The real galaxy images include some initial correlated noise from the original HST observation. However, we whiten the noise of the final image so the final image has stationary Gaussian noise, rather than correlated noise. """ logging.basicConfig(format="%(message)s", level=logging.INFO, stream=sys.stdout) logger = logging.getLogger("demo11") # Define some parameters we'll use below. # Normally these would be read in from some parameter file. pixel_scale = 0.2 # arcsec/pixel image_size = 2048 # size of image in pixels image_size_arcsec = image_size * pixel_scale # size of big image in each dimension (arcsec) noise_variance = 5.e4 # ADU^2 (Just use simple Gaussian noise here.) nobj = 288 # number of galaxies in entire field # (This corresponds to 8 galaxies / arcmin^2) grid_spacing = 90.0 # The spacing between the samples for the power spectrum # realization (arcsec) tel_diam = 4 # Let's figure out the flux for a 4 m class telescope exp_time = 300 # exposing for 300 seconds. center_ra = 19.3 * galsim.hours # The RA, Dec of the center of the image on the sky center_dec = -33.1 * galsim.degrees # The catalog returns objects that are appropriate for HST in 1 second exposures. So for our # telescope we scale up by the relative area and exposure time. Note that what is important is # the *effective* area after taking into account obscuration. For HST, the telescope diameter # is 2.4 but there is obscuration (a linear factor of 0.33). Here, we assume that the telescope # we're simulating effectively has no obscuration factor. We're also ignoring the pi/4 factor # since it appears in the numerator and denominator, so we use area = diam^2. hst_eff_area = 2.4**2 * (1. - 0.33**2) flux_scaling = (tel_diam**2 / hst_eff_area) * exp_time # random_seed is used for both the power spectrum realization and the random properties # of the galaxies. random_seed = 24783923 file_name = os.path.join('output', 'tabulated_power_spectrum.fits.fz') logger.info('Starting demo script 11') # Read in galaxy catalog # The COSMOSCatalog uses the same input file as we have been using for RealGalaxyCatalogs # along with a second file called real_galaxy_catalog_23.5_examples_fits.fits, which stores # the information about the parameteric fits. There is no need to specify the second file # name, since the name is derivable from the name of the main catalog. if True: # The catalog we distribute with the GalSim code only has 100 galaxies. # The galaxies will typically be reused several times here. cat_file_name = 'real_galaxy_catalog_23.5_example.fits' dir = 'data' cosmos_cat = galsim.COSMOSCatalog(cat_file_name, dir=dir) else: # If you've run galsim_download_cosmos, you can leave out the cat_file_name and dir # to use the full COSMOS catalog with 56,000 galaxies in it. cosmos_cat = galsim.COSMOSCatalog() logger.info('Read in %d galaxies from catalog', cosmos_cat.nobjects) # Setup the PowerSpectrum object we'll be using: # To do this, we first have to read in the tabulated shear power spectrum, often denoted # C_ell(ell), where ell has units of inverse angle and C_ell has units of angle^2. However, # GalSim works in the flat-sky approximation, so we use this notation interchangeably with # P(k). GalSim does not calculate shear power spectra for users, who must be able to provide # their own (or use the examples in the repository). # # Here we use a tabulated power spectrum from iCosmo (http://icosmo.org), with the following # cosmological parameters and survey design: # H_0 = 70 km/s/Mpc # Omega_m = 0.25 # Omega_Lambda = 0.75 # w_0 = -1.0 # w_a = 0.0 # n_s = 0.96 # sigma_8 = 0.8 # Smith et al. prescription for the non-linear power spectrum. # Eisenstein & Hu transfer function with wiggles. # Default dN/dz with z_med = 1.0 # The file has, as required, just two columns which are k and P(k). However, iCosmo works in # terms of ell and C_ell; ell is inverse radians and C_ell in radians^2. Since GalSim tends to # work in terms of arcsec, we have to tell it that the inputs are radians^-1 so it can convert # to store in terms of arcsec^-1. pk_file = os.path.join('data', 'cosmo-fid.zmed1.00.out') ps = galsim.PowerSpectrum(pk_file, units=galsim.radians) # The argument here is "e_power_function" which defines the E-mode power to use. logger.info('Set up power spectrum from tabulated P(k)') # Now let's read in the PSF. It's a real SDSS PSF, which means pixel scale of 0.396". However, # the typical seeing is 1.2" and we want to simulate better seeing, so we will just tell GalSim # that the pixel scale is 0.2". We have to be careful with SDSS PSF images, as they have an # added 'soft bias' of 1000 which has been removed before creation of this file, so that the sky # level is properly zero. Also, the file is bzipped, to demonstrate the ability of GalSim # handle this kind of compressed file (among others). We read the image directly into an # InterpolatedImage GSObject, so we can manipulate it as needed (here, the only manipulation # needed is convolution). The flux is 1 as needed for a PSF. psf_file = os.path.join('data', 'example_sdss_psf_sky0.fits.bz2') psf = galsim.InterpolatedImage(psf_file, scale=pixel_scale, flux=1.) logger.info('Read in PSF image from bzipped FITS file') # Setup the image: full_image = galsim.ImageF(image_size, image_size) # The default convention for indexing an image is to follow the FITS standard where the # lower-left pixel is called (1,1). However, this can be counter-intuitive to people more # used to C or python indexing, where indices start at 0. It is possible to change the # coordinates of the lower-left pixel with the methods `setOrigin`. For this demo, we # switch to 0-based indexing, so the lower-left pixel will be called (0,0). full_image.setOrigin(0, 0) # As for demo10, we use random_seed for the random numbers required for the # whole image. In this case, both the power spectrum realization and the noise on the # full image we apply later. rng = galsim.BaseDeviate(random_seed) # We want to make random positions within our image. However, currently for shears from a power # spectrum we first have to get shears on a grid of positions, and then we can choose random # positions within that. So, let's make the grid. We're going to make it as large as the # image, with grid points spaced by 90 arcsec (hence interpolation only happens below 90" # scales, below the interesting scales on which we want the shear power spectrum to be # represented exactly). The lensing engine wants positions in arcsec, so calculate that: ps.buildGrid(grid_spacing=grid_spacing, ngrid=int(math.ceil(image_size_arcsec / grid_spacing)), rng=rng) logger.info('Made gridded shears') # We keep track of how much noise is already in the image from the RealGalaxies. # The default initial value is all pixels = 0. noise_image = galsim.ImageF(image_size, image_size) noise_image.setOrigin(0, 0) # Make a slightly non-trivial WCS. We'll use a slightly rotated coordinate system # and center it at the image center. theta = 0.17 * galsim.degrees # ( dudx dudy ) = ( cos(theta) -sin(theta) ) * pixel_scale # ( dvdx dvdy ) ( sin(theta) cos(theta) ) # Aside: You can call numpy trig functions on Angle objects directly, rather than getting # their values in radians first. Or, if you prefer, you can write things like # theta.sin() or theta.cos(), which are equivalent. dudx = numpy.cos(theta) * pixel_scale dudy = -numpy.sin(theta) * pixel_scale dvdx = numpy.sin(theta) * pixel_scale dvdy = numpy.cos(theta) * pixel_scale image_center = full_image.true_center affine = galsim.AffineTransform(dudx, dudy, dvdx, dvdy, origin=full_image.true_center) # We can also put it on the celestial sphere to give it a bit more realism. # The TAN projection takes a (u,v) coordinate system on a tangent plane and projects # that plane onto the sky using a given point as the tangent point. The tangent # point should be given as a CelestialCoord. sky_center = galsim.CelestialCoord(ra=center_ra, dec=center_dec) # The third parameter, units, defaults to arcsec, but we make it explicit here. # It sets the angular units of the (u,v) intermediate coordinate system. wcs = galsim.TanWCS(affine, sky_center, units=galsim.arcsec) full_image.wcs = wcs # Now we need to loop over our objects: for k in range(nobj): time1 = time.time() # The usual random number generator using a different seed for each galaxy. ud = galsim.UniformDeviate(random_seed + k + 1) # Choose a random RA, Dec around the sky_center. # Note that for this to come out close to a square shape, we need to account for the # cos(dec) part of the metric: ds^2 = dr^2 + r^2 d(dec)^2 + r^2 cos^2(dec) d(ra)^2 # So need to calculate dec first. dec = center_dec + (ud() - 0.5) * image_size_arcsec * galsim.arcsec ra = center_ra + ( ud() - 0.5) * image_size_arcsec / numpy.cos(dec) * galsim.arcsec world_pos = galsim.CelestialCoord(ra, dec) # We will need the image position as well, so use the wcs to get that image_pos = wcs.toImage(world_pos) # We also need this in the tangent plane, which we call "world coordinates" here, # since the PowerSpectrum class is really defined on that plane, not in (ra,dec). uv_pos = affine.toWorld(image_pos) # Get the reduced shears and magnification at this point g1, g2, mu = ps.getLensing(pos=uv_pos) # Now we will have the COSMOSCatalog make a galaxy profile for us. It can make either # a RealGalaxy using the original HST image and PSF, or a parametric model based on # parametric fits to the light distribution of the HST observation. The parametric # models are either a Sersic fit to the data or a bulge + disk fit according to which # one gave the better chisq value. We will select a galaxy at random from the catalog. # One could easily do this by choosing an index = int(ud() * cosmos_cat.nobjects), but # we will instead allow the catalog to choose a random galaxy for us. It will remove any # selection effects involved in postage stamp creation using weights that are stored in # the catalog. (If for some reason you prefer not to do that, you can always choose a # purely random index yourself using int(ud() * cosmos_cat.nobjects).) We employ this # random selection by simply failing to specify an index or identifier for a galaxy, in # which case it chooses a random one. # First determine whether we will make a real galaxy (`gal_type = 'real'`) or a parametric # galaxy (`gal_type = 'parametric'`). The real galaxies take longer to render, so for this # script, we just use them 30% of the time and use parametric galaxies the other 70%. # We could just use `ud()<0.3` for this, but instead we introduce another Deviate type # available in GalSim that we haven't used yet: BinomialDeviate. # It takes an N and p value and returns integers according to a binomial distribution. # i.e. How many heads you get after N flips if each flip has a chance, p, of being heads. binom = galsim.BinomialDeviate(ud, N=1, p=0.3) real = binom() if real: # For real galaxies, we will want to whiten the noise in the image (below). # When whitening the image, we need to make sure the original correlated noise is # present throughout the whole image, otherwise the whitening will do the wrong thing # to the parts of the image that don't include the original image. The RealGalaxy # stores the correct noise profile to use as the gal.noise attribute. This noise # profile is automatically updated as we shear, dilate, convolve, etc. But we need to # tell it how large to pad with this noise by hand. This is a bit complicated for the # code to figure out on its own, so we have to supply the size for noise padding # with the noise_pad_size parameter. # The large galaxies will render fine without any noise padding, but the postage stamp # for the smaller galaxies will be sized appropriately for the PSF, which may make the # stamp larger than the original galaxy image. The psf image is 40 x 40, although # the bright part is much more concentrated than that. If we pad out the galaxy image # to at least 40 x sqrt(2), we should be safe even if the galaxy image is rotated # with respect to the psf image. # noise_pad_size = 40 * sqrt(2) * 0.2 arcsec/pixel = 11.3 arcsec gal = cosmos_cat.makeGalaxy(gal_type='real', rng=ud, noise_pad_size=11.3) else: gal = cosmos_cat.makeGalaxy(gal_type='parametric', rng=ud) # Apply a random rotation theta = ud() * 2.0 * numpy.pi * galsim.radians gal = gal.rotate(theta) # Rescale the flux to match our telescope configuration. # This automatically scales up the noise variance by flux_scaling**2. gal *= flux_scaling # Apply the cosmological (reduced) shear and magnification at this position using a single # GSObject method. gal = gal.lens(g1, g2, mu) # Convolve with the PSF. final = galsim.Convolve(psf, gal) # Account for the fractional part of the position # cf. demo9.py for an explanation of this nominal position stuff. x_nominal = image_pos.x + 0.5 y_nominal = image_pos.y + 0.5 ix_nominal = int(math.floor(x_nominal + 0.5)) iy_nominal = int(math.floor(y_nominal + 0.5)) dx = x_nominal - ix_nominal dy = y_nominal - iy_nominal offset = galsim.PositionD(dx, dy) # We use method='no_pixel' here because the SDSS PSF image that we are using includes the # pixel response already. stamp = final.drawImage(wcs=wcs.local(image_pos), offset=offset, method='no_pixel') # Recenter the stamp at the desired position: stamp.setCenter(ix_nominal, iy_nominal) # Find the overlapping bounds: bounds = stamp.bounds & full_image.bounds # Now, if we are using a real galaxy, we want to ether whiten or at least symmetrize the # noise on the postage stamp to avoid having to deal with correlated noise in any kind of # image processing you would want to do on the final image. (Like measure galaxy shapes.) # Galsim automatically propagates the noise correctly from the initial RealGalaxy object # through the applied shear, distortion, rotation, and convolution into the final object's # noise attribute. To make the noise fully white, use the image.whitenNoise() method. # The returned value is the variance of the Gaussian noise that is present after the # whitening process. # However, this is often overkill for many applications. If it is acceptable to merely end # up with noise with some degree of symmetry (say 4-fold or 8-fold symmetry), then you can # instead have GalSim just add enough noise to make the resulting noise have this kind of # symmetry. Usually this requires adding significantly less additional noise, which means # you can have the resulting total variance be somewhat smaller. The returned variance # corresponds to the zero-lag value of the noise correlation function, which will still have # off-diagonal elements. We can do this step using the image.symmetrizeNoise() method. if real: if True: # We use the symmetrizing option here. new_variance = stamp.symmetrizeNoise(final.noise, 8) else: # Here is how you would do it if you wanted to fully whiten the image. new_variance = stamp.whitenNoise(final.noise) # We need to keep track of how much variance we have currently in the image, so when # we add more noise, we can omit what is already there. noise_image[bounds] += new_variance # Finally, add the stamp to the full image. full_image[bounds] += stamp[bounds] time2 = time.time() tot_time = time2 - time1 logger.info('Galaxy %d: position relative to center = %s, t=%f s', k, str(uv_pos), tot_time) # We already have some noise in the image, but it isn't uniform. So the first thing to do is # to make the Gaussian noise uniform across the whole image. We have a special noise class # that can do this. VariableGaussianNoise takes an image of variance values and applies # Gaussian noise with the corresponding variance to each pixel. # So all we need to do is build an image with how much noise to add to each pixel to get us # up to the maximum value that we already have in the image. max_current_variance = numpy.max(noise_image.array) noise_image = max_current_variance - noise_image vn = galsim.VariableGaussianNoise(rng, noise_image) full_image.addNoise(vn) # Now max_current_variance is the noise level across the full image. We don't want to add that # twice, so subtract off this much from the intended noise that we want to end up in the image. noise_variance -= max_current_variance # Now add Gaussian noise with this variance to the final image. We have to do this step # at the end, rather than adding to individual postage stamps, in order to get the noise # level right in the overlap regions between postage stamps. noise = galsim.GaussianNoise(rng, sigma=math.sqrt(noise_variance)) full_image.addNoise(noise) logger.info('Added noise to final large image') # Now write the image to disk. It is automatically compressed with Rice compression, # since the filename we provide ends in .fz. full_image.write(file_name) logger.info('Wrote image to %r', file_name) # Compute some sky positions of some of the pixels to compare with the values of RA, Dec # that ds9 reports. ds9 always uses (1,1) for the lower left pixel, so the pixel coordinates # of these pixels are different by 1, but you can check that the RA and Dec values are # the same as what GalSim calculates. ra_str = center_ra.hms() dec_str = center_dec.dms() logger.info('Center of image is at RA %sh %sm %ss, DEC %sd %sm %ss', ra_str[0:3], ra_str[3:5], ra_str[5:], dec_str[0:3], dec_str[3:5], dec_str[5:]) for (x, y) in [(0, 0), (0, image_size - 1), (image_size - 1, 0), (image_size - 1, image_size - 1)]: world_pos = wcs.toWorld(galsim.PositionD(x, y)) ra_str = world_pos.ra.hms() dec_str = world_pos.dec.dms() logger.info('Pixel (%4d, %4d) is at RA %sh %sm %ss, DEC %sd %sm %ss', x, y, ra_str[0:3], ra_str[3:5], ra_str[5:], dec_str[0:3], dec_str[3:5], dec_str[5:]) logger.info( 'ds9 reports these pixels as (1,1), (1,2048), etc. with the same RA, Dec.' )
def main(argv): """ Make images using model PSFs and galaxy cluster shear: - The galaxies come from COSMOSCatalog, which can produce either RealGalaxy profiles (like in demo10) and parametric fits to those profiles. - 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) global logger logger = logging.getLogger("mock_superbit_data") # Define some parameters we'll use below. # Normally these would be read in from some parameter file. global pixel_scale pixel_scale = 0.206 # arcsec/pixel global image_xsize image_xsize = 6665 # size of image in pixels global image_ysize image_ysize = 4453 # size of image in pixels global image_xsize_arcsec image_xsize_arcsec = image_xsize*pixel_scale # size of big image in each dimension (arcsec) global image_ysize_arcsec image_ysize_arcsec = image_ysize*pixel_scale # size of big image in each dimension (arcsec) global center_ra center_ra = 19.3*galsim.hours # The RA, Dec of the center of the image on the sky global center_dec center_dec = -33.1*galsim.degrees global nobj nobj = 30 # number of galaxies in entire field; this number matches empirical global nstars nstars = 1000 # number of stars in the entire field global flux_scaling # Let's figure out the flux for a 0.5 m class telescope global tel_diam tel_diam = 0.5 global psf_fwhm psf_fwhm = 0.30 global lam lam = 625 # Central wavelength for an airy disk global exp_time exp_time = 300 global noise_variance global sky_level psf_path = '/Users/jemcclea/Research/SuperBIT/superbit-ngmix/scripts/outputs/psfex_output' global nfw # will store the NFWHalo information global cosmos_cat # will store the COSMOS catalog from which we draw objects # Set up the NFWHalo: mass=5E14 # Cluster mass (Msol/h) nfw_conc = 4 # Concentration parameter = virial radius / NFW scale radius nfw_z_halo = 0.17 # redshift of the halo --> correct! nfw_z_source = 0.6 # redshift of the lensed sources; COSMOS galaxies don't have any omega_m = 0.3 # Omega matter for the background cosmology. omega_lam = 0.7 # Omega lambda for the background cosmology. nfw = galsim.NFWHalo(mass=mass, conc=nfw_conc, redshift=nfw_z_halo, omega_m=omega_m, omega_lam=omega_lam) logger.info('Set up NFW halo for lensing') # Read in galaxy catalog """ cat_file_name = 'real_galaxy_catalog_23.5.fits' dir = 'data/COSMOS_23.5_training_sample' #cat_file_name = 'real_galaxy_catalog_23.5_example.fits' #dir = 'data' """ cat_file_name = 'real_galaxy_catalog_25.2.fits' dir = 'data/COSMOS_25.2_training_sample/' cosmos_cat = galsim.COSMOSCatalog(cat_file_name, dir=dir) logger.info('Read in %d galaxies from catalog', cosmos_cat.nobjects) # 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. # Will also multiply by the gain and relative pixel scales... hst_eff_area = 2.4**2 * (1.-0.33**2) sbit_eff_area = tel_diam**2 * (1.-0.3840**2) #sbit_eff_area = tel_diam**2 * (1.-0.1**2) ### ### LOOP OVER PSFs TO MAKE GROUPS OF IMAGES ### WITHIN EACH PSF, ITERATE 5 TIMES TO MAKE 5 SEPARATE IMAGES ### #all_psfs=glob.glob(psf_path+"/*.psf") #all_psfs=glob.glob(psf_path+"/*300*.psf") random_seed = 35609377914 i=0 for psf_filen in range(1): logger.info('Beginning PSF %s...'% psf_filen) rng = galsim.BaseDeviate(random_seed) timescale=str(exp_time) outname=''.join(['debug_0.3FWHM_gaussStar_',timescale,'_',str(i),'.fits']) truth_file_name=''.join(['./output-debug/truth_0.3FWHM_gaussStar_',timescale,'_',str(i),'.dat']) file_name = os.path.join('output-debug',outname) # Set up the image: if timescale=='150': print("Automatically detecting a 150s exposure image, setting flux scale and noise accordingly") #noise_variance=570 # ADU^2 (Just use simple Gaussian noise here.) noise_variance=570 # ADU^2 (Just use simple Gaussian noise here.) sky_level = 51 # ADU exp_time=150. else: print("Automatically detecting a 300s exposure image, setting flux scale and noise accordingly") #noise_variance=400 # ADU^2 (Just use simple Gaussian noise here.) noise_variance=400 # ADU^2 (Just use simple Gaussian noise here.) sky_level = 106 # ADU exp_time=300. flux_scaling = (sbit_eff_area/hst_eff_area) * exp_time * 3.33 * (.206/.05)**2 # Setting up a truth catalog names = [ 'gal_num', 'x_image', 'y_image', 'ra', 'dec', 'g1_nopsf', 'g2_nopsf','g1_meas', 'g2_meas', 'fwhm','final_sigmaSize', 'nopsf_sigmaSize','nfw_g1', 'nfw_g2', 'nfw_mu', 'redshift','flux', 'stamp_sum', 'noisevar'] types = [ int, float, float, float, float, float, float, float, float, float, float, float, float, float,float, float, float,float, float] truth_catalog = galsim.OutputCatalog(names, types) # Set up the image: full_image = galsim.ImageF(image_xsize, image_ysize) full_image.fill(sky_level) full_image.setOrigin(0,0) # We keep track of how much noise is already in the image from the RealGalaxies. noise_image = galsim.ImageF(image_xsize, image_ysize) 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 = numpy.cos(theta) * pixel_scale dudy = -numpy.sin(theta) * pixel_scale dvdx = numpy.sin(theta) * pixel_scale dvdy = numpy.cos(theta) * pixel_scale image_center = full_image.true_center affine = galsim.AffineTransform(dudx, dudy, dvdx, dvdy, origin=full_image.true_center) sky_center = galsim.CelestialCoord(ra=center_ra, dec=center_dec) wcs = galsim.TanWCS(affine, sky_center, units=galsim.arcsec) full_image.wcs = wcs # Loop over galaxy 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) try: # make single galaxy object logger.debug("about to make stamp %d...",k) stamp,truth = make_a_galaxy(ud=ud,wcs=wcs,affine=affine) logger.debug("stamp %d is made",k) # Find the overlapping bounds: bounds = stamp.bounds & full_image.bounds # We need to keep track of how much variance we have currently in the image, so when # we add more noise, we can omit what is already there. noise_image[bounds] += truth.variance # Finally, add the stamp to the full image. full_image[bounds] += stamp[bounds] logger.debug("stamp %d added to full image",k) time2 = time.time() tot_time = time2-time1 logger.info('Galaxy %d positioned relative to center t=%f s', k, tot_time) try: g1_real=stamp.FindAdaptiveMom().observed_shape.g1 g2_real=stamp.FindAdaptiveMom().observed_shape.g2 except: g1_real=-9999. g2_real=-9999. logger.debug("Galaxy %d made it past g1/g2_real stage",k) sum_flux=numpy.sum(stamp.array) row = [ k,truth.x, truth.y, truth.ra, truth.dec, truth.g1_nopsf, truth.g2_nopsf, g1_real, g2_real, truth.fwhm, truth.final_sigmaSize, truth.nopsf_sigmaSize,truth.g1,truth.g2, truth.mu, truth.z, truth.flux, sum_flux, truth.variance] truth_catalog.addRow(row) logger.debug("row for galaxy %d added to truth catalog\n\n",k) except: logger.info('Galaxy %d has failed, skipping...',k) #pdb.set_trace() pass ###### Inject cluster galaxy objects: random_seed=892465352 center_coords = galsim.CelestialCoord(center_ra,center_dec) centerpix = wcs.toImage(center_coords) for k in range(40): time1 = time.time() # The usual random number generator using a different seed for each galaxy. ud = galsim.UniformDeviate(random_seed+k+1) try: # make single galaxy object cluster_stamp,truth = make_cluster_galaxy(ud=ud,wcs=wcs,affine=affine,centerpix=centerpix) # Find the overlapping bounds: bounds = cluster_stamp.bounds & full_image.bounds # We need to keep track of how much variance we have currently in the image, so when # we add more noise, we can omit what is already there. noise_image[bounds] += truth.variance # Finally, add the stamp to the full image. full_image[bounds] += cluster_stamp[bounds] time2 = time.time() tot_time = time2-time1 logger.info('Cluster galaxy %d positioned relative to center t=%f s', k, tot_time) except: logger.info('Cluster galaxy %d has failed, skipping...',k) pdb.set_trace() #### ### Now repeat process for stars! #### random_seed_stars=2308173501873 for k in range(nstars): time1 = time.time() ud = galsim.UniformDeviate(random_seed_stars+k+1) try: star_stamp,truth=make_a_star(ud=ud,wcs=wcs,affine=affine) bounds = star_stamp.bounds & full_image.bounds logger.debug("star stamp & truth catalog made for star %d" %k) # Add the stamp to the full image. full_image[bounds] += star_stamp[bounds] time2 = time.time() tot_time = time2-time1 logger.info('Star %d: positioned relative to center, t=%f s', k, tot_time) try: g1_real=star_stamp.FindAdaptiveMom().observed_shape.g1 g2_real=star_stamp.FindAdaptiveMom().observed_shape.g2 except: g1_real = -9999. g2_real = -9999. this_var = -9999. sum_flux=numpy.sum(star_stamp.array) row = [ k,truth.x, truth.y, truth.ra, truth.dec, truth.g1_nopsf, truth.g2_nopsf, g1_real, g2_real, truth.fwhm, truth.final_sigmaSize, truth.nopsf_sigmaSize, truth.g1, truth.g2, truth.mu, truth.z, truth.flux, sum_flux, truth.variance] truth_catalog.addRow(row) except: logger.info('Star %d has failed, skipping...',k) pdb.set_trace() # 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. #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. noise = galsim.GaussianNoise(rng, sigma=math.sqrt(noise_variance)) full_image.addNoise(noise) logger.info('Added noise to final output image') # Now write the image to disk. full_image.write(file_name) # Add a FLUXSCL keyword for later stacking this_hdu=astropy.io.fits.open(file_name) this_hdu[0].header['FLXSCALE'] = 300.0/exp_time this_hdu.writeto(file_name,overwrite='True') logger.info('Wrote image to %r',file_name) # Write truth catalog to file. truth_catalog.write(truth_file_name) i=i+1 logger.info('completed run %d for psf %s',i,psf_filen) logger.info('completed all images')
def test_variable_gaussian_noise(): """Test VariableGaussian random number generator """ # Make a checkerboard image with two values for the variance gSigma1 = 17.23 gSigma2 = 28.55 var_image = galsim.ImageD(galsim.BoundsI(0, 9, 0, 9)) coords = np.ogrid[0:10, 0:10] var_image.array[(coords[0] + coords[1]) % 2 == 1] = gSigma1**2 var_image.array[(coords[0] + coords[1]) % 2 == 0] = gSigma2**2 print('var_image.array = ', var_image.array) g = galsim.GaussianDeviate(testseed, sigma=1.) vgResult = np.empty((10, 10)) g.generate(vgResult) vgResult *= np.sqrt(var_image.array) # Test filling an image vgn = galsim.VariableGaussianNoise(galsim.BaseDeviate(testseed), var_image) testimage = galsim.ImageD(10, 10) testimage.addNoise(vgn) np.testing.assert_array_almost_equal( testimage.array, vgResult, precision, err_msg= "VariableGaussianNoise applied to Images does not reproduce expected sequence" ) # Test filling an image with Fortran ordering vgn.rng.seed(testseed) testimage = galsim.ImageD(np.zeros((10, 10)).T) testimage.addNoise(vgn) np.testing.assert_array_almost_equal( testimage.array, vgResult, precision, err_msg= "Wrong VariableGaussian noise generated for Fortran-ordered Image") # Check var_image property np.testing.assert_almost_equal( vgn.var_image.array, var_image.array, precision, err_msg="VariableGaussianNoise var_image returns wrong var_image") # Check that the noise model really does produce this variance. big_var_image = galsim.ImageD(galsim.BoundsI(0, 2047, 0, 2047)) big_coords = np.ogrid[0:2048, 0:2048] mask1 = (big_coords[0] + big_coords[1]) % 2 == 0 mask2 = (big_coords[0] + big_coords[1]) % 2 == 1 big_var_image.array[mask1] = gSigma1**2 big_var_image.array[mask2] = gSigma2**2 big_vgn = galsim.VariableGaussianNoise(galsim.BaseDeviate(testseed), big_var_image) big_im = galsim.Image(2048, 2048, dtype=float) big_im.addNoise(big_vgn) var = np.var(big_im.array) print('variance = ', var) print('getVar = ', big_vgn.var_image.array.mean()) np.testing.assert_almost_equal( var, big_vgn.var_image.array.mean(), 1, err_msg= 'Realized variance for VariableGaussianNoise did not match var_image') # Check realized variance in each mask print('rms1 = ', np.std(big_im.array[mask1])) print('rms2 = ', np.std(big_im.array[mask2])) np.testing.assert_almost_equal(np.std(big_im.array[mask1]), gSigma1, decimal=1) np.testing.assert_almost_equal(np.std(big_im.array[mask2]), gSigma2, decimal=1) # Check that VariableGaussianNoise adds to the image, not overwrites the image. gal = galsim.Exponential(half_light_radius=2.3, flux=1.e4) gal.drawImage(image=big_im) big_vgn.rng.seed(testseed) big_im.addNoise(big_vgn) gal.withFlux(-1.e4).drawImage(image=big_im, add_to_image=True) var = np.var(big_im.array) np.testing.assert_almost_equal( var, big_vgn.var_image.array.mean(), 1, err_msg= 'VariableGaussianNoise wrong when already an object drawn on the image' ) # Check picklability do_pickle(vgn, lambda x: (x.rng.serialize(), x.var_image)) do_pickle(vgn, drawNoise) do_pickle(vgn) # Check copy, eq and ne vgn2 = galsim.VariableGaussianNoise(vgn.rng.duplicate(), var_image) vgn3 = vgn.copy() vgn4 = vgn.copy(rng=galsim.BaseDeviate(11)) vgn5 = galsim.VariableGaussianNoise(vgn.rng, 2. * var_image) assert vgn == vgn2 assert vgn == vgn3 assert vgn != vgn4 assert vgn != vgn5 assert vgn.rng.raw() == vgn2.rng.raw() assert vgn == vgn2 assert vgn == vgn3 vgn.rng.raw() assert vgn != vgn2 assert vgn == vgn3 assert_raises(TypeError, vgn.applyTo, 23) assert_raises(ValueError, vgn.applyTo, galsim.ImageF(3, 3)) assert_raises(galsim.GalSimError, vgn.getVariance) assert_raises(galsim.GalSimError, vgn.withVariance, 23) assert_raises(galsim.GalSimError, vgn.withScaledVariance, 23)
def BuildTiledImage(config, logger=None, image_num=0, obj_num=0, make_psf_image=False, make_weight_image=False, make_badpix_image=False): """ Build an Image consisting of a tiled array of postage stamps. @param config A configuration dict. @param logger If given, a logger object to log progress. [default: None] @param image_num If given, the current `image_num`. [default: 0] @param obj_num If given, the current `obj_num`. [default: 0] @param make_psf_image Whether to make `psf_image`. [default: False] @param make_weight_image Whether to make `weight_image`. [default: False] @param make_badpix_image Whether to make `badpix_image`. [default: False] @returns the tuple `(image, psf_image, weight_image, badpix_image)`. Note: All 4 Images are always returned in the return tuple, but the latter 3 might be None depending on the parameters make_*_image. """ config['index_key'] = 'image_num' config['image_num'] = image_num config['obj_num'] = obj_num if logger: logger.debug('image %d: BuildTiledImage: image, obj = %d,%d',image_num,image_num,obj_num) if 'random_seed' in config['image'] and not isinstance(config['image']['random_seed'],dict): first = galsim.config.ParseValue(config['image'], 'random_seed', config, int)[0] config['image']['random_seed'] = { 'type' : 'Sequence', 'first' : first } ignore = [ 'random_seed', 'draw_method', 'noise', 'pixel_scale', 'wcs', 'nproc', 'sky_level', 'sky_level_pixel', 'retry_failures', 'image_pos', 'n_photons', 'wmult', 'offset', 'gsparams' ] req = { 'nx_tiles' : int , 'ny_tiles' : int } opt = { 'stamp_size' : int , 'stamp_xsize' : int , 'stamp_ysize' : int , 'border' : int , 'xborder' : int , 'yborder' : int , 'nproc' : int , 'index_convention' : str, 'order' : str } params = galsim.config.GetAllParams( config['image'], 'image', config, req=req, opt=opt, ignore=ignore)[0] nx_tiles = params['nx_tiles'] ny_tiles = params['ny_tiles'] nobjects = nx_tiles * ny_tiles config['nx_tiles'] = nx_tiles config['ny_tiles'] = ny_tiles if logger: logger.debug('image %d: n_tiles = %d, %d',image_num,nx_tiles,ny_tiles) stamp_size = params.get('stamp_size',0) stamp_xsize = params.get('stamp_xsize',stamp_size) stamp_ysize = params.get('stamp_ysize',stamp_size) config['tile_xsize'] = stamp_xsize config['tile_ysize'] = stamp_ysize if (stamp_xsize == 0) or (stamp_ysize == 0): raise AttributeError( "Both image.stamp_xsize and image.stamp_ysize need to be defined and != 0.") border = params.get("border",0) xborder = params.get("xborder",border) yborder = params.get("yborder",border) do_noise = xborder >= 0 and yborder >= 0 # TODO: Note: if one of these is < 0 and the other is > 0, then # this will add noise to the border region. Not exactly the # design, but I didn't bother to do the bookkeeping right to # make the borders pure 0 in that case. full_xsize = (stamp_xsize + xborder) * nx_tiles - xborder full_ysize = (stamp_ysize + yborder) * ny_tiles - yborder # If image_force_xsize and image_force_ysize were set in config, make sure it matches. if ( ('image_force_xsize' in config and full_xsize != config['image_force_xsize']) or ('image_force_ysize' in config and full_ysize != config['image_force_ysize']) ): raise ValueError( "Unable to reconcile required image xsize and ysize with provided "+ "nx_tiles=%d, ny_tiles=%d, "%(nx_tiles,ny_tiles) + "xborder=%d, yborder=%d\n"%(xborder,yborder) + "Calculated full_size = (%d,%d) "%(full_xsize,full_ysize)+ "!= required (%d,%d)."%(config['image_force_xsize'],config['image_force_ysize'])) config['image_xsize'] = full_xsize config['image_ysize'] = full_ysize if logger: logger.debug('image %d: image_size = %d, %d',image_num,full_xsize,full_ysize) convention = params.get('index_convention','1') _set_image_origin(config,convention) if logger: logger.debug('image %d: image_origin = %s',image_num,str(config['image_origin'])) logger.debug('image %d: image_center = %s',image_num,str(config['image_center'])) wcs = galsim.config.BuildWCS(config, logger) # Set the rng to use for image stuff. if 'random_seed' in config['image']: # Technically obj_num+nobjects will be the index of the random seed used for the next # image's first object (if there is a next image). But I don't think that will have # any adverse effects. config['obj_num'] = obj_num + nobjects config['index_key'] = 'obj_num' seed = galsim.config.ParseValue(config['image'], 'random_seed', config, int)[0] config['index_key'] = 'image_num' if logger: logger.debug('image %d: seed = %d',image_num,seed) rng = galsim.BaseDeviate(seed) else: rng = galsim.BaseDeviate() config['rng'] = rng # Make a list of ix,iy values according to the specified order: order = params.get('order','row').lower() if order.startswith('row'): ix_list = [ ix for iy in range(ny_tiles) for ix in range(nx_tiles) ] iy_list = [ iy for iy in range(ny_tiles) for ix in range(nx_tiles) ] elif order.startswith('col'): ix_list = [ ix for ix in range(nx_tiles) for iy in range(ny_tiles) ] iy_list = [ iy for ix in range(nx_tiles) for iy in range(ny_tiles) ] elif order.startswith('rand'): ix_list = [ ix for ix in range(nx_tiles) for iy in range(ny_tiles) ] iy_list = [ iy for ix in range(nx_tiles) for iy in range(ny_tiles) ] galsim.random.permute(rng, ix_list, iy_list) # Define a 'image_pos' field so the stamps can set their position appropriately in case # we need it for PowerSpectum or NFWHalo. x0 = (stamp_xsize-1)/2. + config['image_origin'].x y0 = (stamp_ysize-1)/2. + config['image_origin'].y dx = stamp_xsize + xborder dy = stamp_ysize + yborder config['image']['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 ] } } nproc = params.get('nproc',1) full_image = galsim.ImageF(full_xsize, full_ysize) full_image.setOrigin(config['image_origin']) full_image.wcs = wcs full_image.setZero() if make_psf_image: full_psf_image = galsim.ImageF(full_image.bounds, wcs=wcs) full_psf_image.setZero() else: full_psf_image = None if make_weight_image: full_weight_image = galsim.ImageF(full_image.bounds, wcs=wcs) full_weight_image.setZero() else: full_weight_image = None if make_badpix_image: full_badpix_image = galsim.ImageS(full_image.bounds, wcs=wcs) full_badpix_image.setZero() else: full_badpix_image = None # Sometimes an input field needs to do something special at the start of an image. if 'input' in config: for key in [ k for k in galsim.config.valid_input_types.keys() if k in config['input'] ]: if galsim.config.valid_input_types[key][4]: assert key in config fields = config['input'][key] if not isinstance(fields, list): fields = [ fields ] input_objs = config[key] for i in range(len(fields)): field = fields[i] input_obj = input_objs[i] func = eval(galsim.config.valid_input_types[key][4]) func(input_obj, field, config) stamp_images = galsim.config.BuildStamps( nobjects=nobjects, config=config, nproc=nproc, logger=logger, obj_num=obj_num, xsize=stamp_xsize, ysize=stamp_ysize, do_noise=do_noise, make_psf_image=make_psf_image, make_weight_image=make_weight_image, make_badpix_image=make_badpix_image) images = stamp_images[0] psf_images = stamp_images[1] weight_images = stamp_images[2] badpix_images = stamp_images[3] current_vars = stamp_images[4] max_current_var = 0 for k in range(nobjects): # This is our signal that the object was skipped. if not images[k].bounds.isDefined(): continue if False: 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(images[k].bounds)) assert full_image.bounds.includes(images[k].bounds) b = images[k].bounds full_image[b] += images[k] if make_psf_image: full_psf_image[b] += psf_images[k] if make_weight_image: full_weight_image[b] += weight_images[k] if make_badpix_image: full_badpix_image[b] |= badpix_images[k] if current_vars[k] > max_current_var: max_current_var = current_vars[k] # Mark that we are no longer doing a single galaxy by deleting image_pos from config top # level, so it cannot be used for things like wcs.pixelArea(image_pos). if 'image_pos' in config: del config['image_pos'] # If didn't do noise above in the stamps, then need to do it here. if not do_noise: if 'noise' in config['image']: # If we didn't apply noise in each stamp, then we need to apply it now. draw_method = galsim.config.GetCurrentValue(config['image'],'draw_method') if max_current_var > 0: import numpy # Then there was whitening applied in the individual stamps. # But there could be a different variance in each postage stamp, so the first # thing we need to do is bring everything up to a common level. noise_image = galsim.ImageF(full_image.bounds) for k in range(nobjects): noise_image[images[k].bounds] += current_vars[k] # Update this, since overlapping postage stamps may have led to a larger # value in some pixels. max_current_var = numpy.max(noise_image.array) # Figure out how much noise we need to add to each pixel. noise_image = max_current_var - noise_image # Add it. full_image.addNoise(galsim.VariableGaussianNoise(rng,noise_image)) # Now max_current_var is how much noise is in each pixel. config['rng'] = rng galsim.config.AddNoise( config,draw_method,full_image,full_weight_image,max_current_var,logger) else: # If we aren't doing noise, we still may need to add a non-zero sky_level. # The same noise function does this with the 'skip' draw method. galsim.config.AddNoise( config,'skip',full_image,full_weight_image,max_current_var,logger) return full_image, full_psf_image, full_weight_image, full_badpix_image
def measure_psfex_shapes(df, psfex_file, image_file, weight_file, noweight, wcs, use_ngmix, fwhm, logger): """Measure shapes of the PSFEx solution at each location. """ logger.info('Read in PSFEx file: %s',psfex_file) ind = df.index[df['FLAGS_PSF'] == 0] logger.info('ind = %s',ind) n_psf = len(ind) logger.info('n_psf = %s',n_psf) df['psfex_dx'] = [ -999. ] * len(df) df['psfex_dy'] = [ -999. ] * len(df) df['psfex_e1'] = [ -999. ] * len(df) df['psfex_e2'] = [ -999. ] * len(df) df['psfex_T'] = [ -999. ] * len(df) df['psfex_flux'] = [ -999. ] * len(df) df['psfex_flag'] = [ NOT_STAR ] * len(df) df.loc[ind, 'psfex_flag'] = 0 if 'reserve' in df: df.loc[df['reserve'], 'psfex_flag'] |= RESERVED #df.loc[~df['use'], 'psfex_flag'] |= NOT_USED try: psf = galsim.des.DES_PSFEx(psfex_file, image_file) except Exception as e: logger.info('Caught %s',e) df.loc[ind, 'psfex_flag'] = FAILURE return full_image = galsim.fits.read(image_file, hdu=0) if wcs is not None: full_image.wcs = wcs if not noweight: #print("want weights! ", weight_file) #weight_file = image_file.replace(".fits", ".weight.fits") full_weight = galsim.fits.read(weight_file, hdu=0) full_weight.array[full_weight.array < 0] = 0. stamp_size = 48 for i in ind: x = df['X_IMAGE'].iloc[i] y = df['Y_IMAGE'].iloc[i] #print('Measure PSFEx model shape at ',x,y) image_pos = galsim.PositionD(x,y) psf_i = psf.getPSF(image_pos) b = galsim.BoundsI(int(x)-stamp_size/2, int(x)+stamp_size/2, int(y)-stamp_size/2, int(y)+stamp_size/2) b = b & full_image.bounds im = full_image[b] im = psf_i.drawImage(image=im, method='no_pixel') im *= df['obs_flux'].iloc[i] if noweight: wt = None else: wt = full_weight[b] var = wt.copy() var.invertSelf() im.addNoise(galsim.VariableGaussianNoise(rng, var)) if use_ngmix: dx, dy, e1, e2, T, flux, flag = ngmix_fit(im, wt, fwhm, x, y, logger, df['FLAGS_PSF'][i]) else: dx, dy, e1, e2, T, flux, flag = hsm(im, wt, logger) #dx, dy, e1, e2, T, flux, flag = hsm(im, wt, logger) if np.any(np.isnan([dx,dy,e1,e2,T,flux])): logger.info(' *** NaN detected (%f,%f,%f,%f,%f,%f).',dx,dy,e1,e2,T,flux) flag |= BAD_MEASUREMENT else: df.loc[i, 'psfex_dx'] = dx df.loc[i, 'psfex_dy'] = dy df.loc[i, 'psfex_e1'] = e1 df.loc[i, 'psfex_e2'] = e2 df.loc[i, 'psfex_T'] = T df.loc[i, 'psfex_flux'] = flux df.loc[i, 'psfex_flag'] |= flag #print('final psfex_flag = %s',df['psfex_flag'][ind].values) logger.info('final psfex_flag = %s',df['psfex_flag'][ind].values)
def main(argv): """ Make images using model PSFs and galaxy cluster shear: - The galaxies come from COSMOSCatalog, which can produce either RealGalaxy profiles (like in demo10) and parametric fits to those profiles. We choose 40% 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("mock_superbit_data") # Define some parameters we'll use below. # Normally these would be read in from some parameter file. global pixel_scale pixel_scale = 0.206 # arcsec/pixel global image_xsize image_xsize = 6665 # size of image in pixels global image_ysize image_ysize = 4453 # size of image in pixels global image_xsize_arcsec image_xsize_arcsec = image_xsize * pixel_scale # size of big image in each dimension (arcsec) global image_ysize_arcsec image_ysize_arcsec = image_ysize * pixel_scale # size of big image in each dimension (arcsec) global center_ra #center_ra = 19.3*galsim.hours # The RA, Dec of the center of the image on the sky global center_dec #center_dec = -33.1*galsim.degrees global nobj nobj = 1700 # number of galaxies in entire field -- an adjustment to ensure ~1100 detections global nstars nstars = 370 # number of stars in the entire field #global flux_scaling # Let's figure out the flux for a 0.5 m class telescope global tel_diam tel_diam = 0.5 global lam lam = 587 # Central wavelength global exp_time global noise_variance global sky_level psf_path = '/Users/jemcclea/Research/SuperBIT_2019/superbit-ngmix/scripts/outputs/psfex_output' global nfw # will store the NFWHalo information global cosmos_cat # will store the COSMOS catalog from which we draw objects # Set up the NFWHalo: mass = 5E14 # Cluster mass (Msol/h) nfw_conc = 4 # Concentration parameter = virial radius / NFW scale radius nfw_z_halo = 0.3 # redshift of the halo nfw_z_source = 0.6 # redshift of the lensed sources omega_m = 0.3 # Omega matter for the background cosmology. omega_lam = 0.7 # Omega lambda for the background cosmology. nfw = galsim.NFWHalo(mass=mass, conc=nfw_conc, redshift=nfw_z_halo, omega_m=omega_m, omega_lam=omega_lam) logger.info('Set up NFW halo for lensing') # Read in galaxy 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) # 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. hst_eff_area = 2.4**2 * (1. - 0.33**2) sbit_eff_area = tel_diam**2 * (1. - 0.3840**2) #flux_scaling = (sbit_eff_area/hst_eff_area) * exp_time ### ### LOOP OVER PSFs TO MAKE GROUPS OF IMAGES ### WITHIN EACH PSF, ITERATE 5 TIMES TO MAKE 5 SEPARATE IMAGES ### all_psfs = glob.glob(psf_path + "/*150*.psf") logger.info('Beginning loop over jitter/optical psfs') # random_seed = 24783923 random_seed = 247 i = 0 for psf_filen in all_psfs: logger.info('Beginning PSF %s...' % psf_filen) rng = galsim.BaseDeviate(random_seed) # This is specific to empirical PSFs try: timescale = psf_filen.split('target_')[1].split('_WCS')[0] except: timescale = psf_filen.split('sci_')[1].split('_WCS')[0] outname = ''.join( ['mockSuperbit_empiricalPSF_', timescale, '_', str(i), '.fits']) truth_file_name = ''.join( ['./output/truth_empiricalPSF_', timescale, '_', str(i), '.dat']) file_name = os.path.join('output', outname) # Set up the image: if timescale == '150': print( "Automatically detecting a 150s exposure image, setting flux scale and noise accordingly" ) noise_variance = 1.8e3 # ADU^2 (Just use simple Gaussian noise here.) -->150s sky_level = 51 # ADU / arcsec^2 -->150s exp_time = 150. else: print( "Automatically detecting a 300s exposure image, setting flux scale and noise accordingly" ) noise_variance = 2.55e3 # ADU^2 (Just use simple Gaussian noise here.) -->300s sky_level = 106 # ADU / arcsec^2 -->300s exp_time = 300. # Setting up a truth catalog names = [ 'gal_num', 'x_image', 'y_image', 'ra', 'dec', 'g1_meas', 'g2_meas', 'nfw_g1', 'nfw_g2', 'nfw_mu', 'redshift', 'flux', 'var' ] types = [ int, float, float, float, float, float, float, float, float, float, float, float, float ] truth_catalog = galsim.OutputCatalog(names, types) # Set up the image: full_image = galsim.ImageF(image_xsize, image_ysize) full_image.fill(sky_level) full_image.setOrigin(0, 0) # We keep track of how much noise is already in the image from the RealGalaxies. noise_image = galsim.ImageF(image_xsize, image_ysize) 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 = numpy.cos(theta) * pixel_scale dudy = -numpy.sin(theta) * pixel_scale dvdx = numpy.sin(theta) * pixel_scale dvdy = numpy.cos(theta) * pixel_scale image_center = full_image.true_center affine = galsim.AffineTransform(dudx, dudy, dvdx, dvdy, origin=full_image.true_center) sky_center = galsim.CelestialCoord(ra=center_ra, dec=center_dec) wcs = galsim.TanWCS(affine, sky_center, units=galsim.arcsec) """ this_im_wcs = get_wcs_info(psf_filen) affine = this_im_wcs.affine(full_image.true_center) full_image.wcs = this_im_wcs # Now let's read in the PSFEx PSF model. We read the image directly into an # InterpolatedImage GSObject, so we can manipulate it as needed psf_wcs = this_im_wcs psf_file = os.path.join(psf_path, psf_filen) psf = galsim.des.DES_PSFEx(psf_file, wcs=psf_wcs) logger.info('Constructed PSF object from PSFEx file') # Loop over galaxy 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) try: # make single galaxy object stamp, truth = make_a_galaxy(ud=ud, this_im_wcs=this_im_wcs, psf=psf, affine=affine) # Find the overlapping bounds: bounds = stamp.bounds & full_image.bounds # We need to keep track of how much variance we have currently in the image, so when # we add more noise, we can omit what is already there. noise_image[bounds] += truth.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 positioned relative to center t=%f s', k, tot_time) g1_real = stamp.FindAdaptiveMom().observed_shape.g1 g2_real = stamp.FindAdaptiveMom().observed_shape.g2 #g1_real=-9999. #g2_real=-9999. row = [ k, truth.x, truth.y, truth.ra, truth.dec, g1_real, g2_real, truth.g1, truth.g2, truth.mu, truth.z, truth.flux, truth.variance ] truth_catalog.addRow(row) except: logger.info('Galaxy %d has failed, skipping...', k) #pdb.set_trace() pass #### ### Now repeat process for stars! #### random_seed_stars = 3221987 for k in range(nstars): time1 = time.time() ud = galsim.UniformDeviate(random_seed_stars + k + 1) star_stamp, truth = make_a_star(ud=ud, this_im_wcs=this_im_wcs, psf=psf, affine=affine) bounds = star_stamp.bounds & full_image.bounds # Add the stamp to the full image. try: full_image[bounds] += star_stamp[bounds] time2 = time.time() tot_time = time2 - time1 logger.info('Star %d: positioned relative to center, t=%f s', k, tot_time) g1_real = star_stamp.FindAdaptiveMom().observed_shape.g1 g2_real = star_stamp.FindAdaptiveMom().observed_shape.g2 #g1_real = -9999. #g2_real = -9999. this_var = -9999. this_flux = numpy.sum(star_stamp.array) row = [ k, truth.x, truth.y, truth.ra, truth.dec, g1_real, g2_real, truth.g1, truth.g2, truth.mu, truth.z, this_flux, this_var ] truth_catalog.addRow(row) except: logger.info('Star %d has failed, skipping...', k) pass # 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. 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. try: noise = galsim.GaussianNoise(rng, sigma=math.sqrt(noise_variance)) except: noise = galsim.GaussianNoise(rng, sigma=math.sqrt(1800)) 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) # Write truth catalog to file. truth_catalog.write(truth_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. i = i + 1 logger.info(' ') logger.info('completed run %d for psf %s', i, psf_filen) logger.info('completed all images')
def main(argv): """ Make images using model PSFs and galaxy cluster shear: - The galaxies come from COSMOSCatalog, which can produce either RealGalaxy profiles (like in demo10) and parametric fits to those profiles. - Using parametric galaxies so that filter responses/system throughput can be convolved """ logging.basicConfig(format="%(message)s", level=logging.INFO, stream=sys.stdout) global logger logger = logging.getLogger("mock_superbit_data") # Define some parameters we'll use below. # Normally these would be read in from some parameter file. global pixel_scale pixel_scale = 0.206 # arcsec/pixel global image_xsize image_xsize = 6665 # size of image in pixels global image_ysize image_ysize = 4453 # size of image in pixels global image_xsize_arcsec image_xsize_arcsec = image_xsize*pixel_scale # size of big image in each dimension (arcsec) global image_ysize_arcsec image_ysize_arcsec = image_ysize*pixel_scale # size of big image in each dimension (arcsec) global center_ra center_ra = 19.3*galsim.hours # The RA, Dec of the center of the image on the sky global center_dec center_dec = -33.1*galsim.degrees global nobj nobj = 2200 # number of galaxies in entire field global nstars nstars = 350 # number of stars in the entire field global flux_scaling # Let's figure out the flux for a 0.5 m class telescope global tel_diam tel_diam = 0.5 global lam lam = 625 # Central wavelength for an airy disk global exp_time global noise_variance global sky_level psf_path = '/Users/jemcclea/Research/SuperBIT/superbit-ngmix/scripts/output-real/psfex_output' global nfw # will store the NFWHalo information global cosmos_cat # will store the COSMOS catalog from which we draw objects # Set up the NFWHalo: mass=5E14 # Cluster mass (Msol/h) nfw_conc = 4 # Concentration parameter = virial radius / NFW scale radius nfw_z_halo = 0.17 # redshift of the halo nfw_z_source = 0.6 # redshift of the lensed sources omega_m = 0.3 # Omega matter for the background cosmology. omega_lam = 0.7 # Omega lambda for the background cosmology. nfw = galsim.NFWHalo(mass=mass, conc=nfw_conc, redshift=nfw_z_halo, omega_m=omega_m, omega_lam=omega_lam) logger.info('Set up NFW halo for lensing') # Read in galaxy catalog cat_file_name = 'real_galaxy_catalog_23.5.fits' dir = 'data/COSMOS_23.5_training_sample' cosmos_cat = galsim.COSMOSCatalog(cat_file_name, dir=dir) logger.info('Read in %d galaxies from catalog', cosmos_cat.nobjects) # 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. # Will also multiply by the gain and relative pixel scales... hst_eff_area = 2.4**2 * (1.-0.33**2) sbit_eff_area = tel_diam**2 * (1.-0.3840**2) ### ### LOOP OVER PSFs TO MAKE GROUPS OF IMAGES ### WITHIN EACH PSF, ITERATE 5 TIMES TO MAKE 5 SEPARATE IMAGES ### all_psfs=glob.glob(psf_path+"/*300*.psf") logger.info('Beginning loop over jitter/optical psfs') random_seed = 4783923 i=0 for psf_filen in all_psfs: logger.info('Beginning PSF %s...'% psf_filen) rng = galsim.BaseDeviate(random_seed) # This is specific to empirical PSFs try: timescale=psf_filen.split('target_')[1].split('_WCS')[0] except: timescale=psf_filen.split('sci_')[1].split('_WCS')[0] outname=''.join(['mockSuperbit_bp_empiricalPSF_',timescale,'_',str(i),'.fits']) truth_file_name=''.join(['./output-bandpass/truth_bp_empiricalPSF_',timescale,'_',str(i),'.dat']) file_name = os.path.join('output-bandpass',outname) # Set up the image: if timescale=='150': print("Automatically detecting a 150s exposure image, setting flux scale and noise accordingly") noise_variance=570 # ADU^2 (Just use simple Gaussian noise here.) sky_level = 51 # ADU exp_time=150. else: print("Automatically detecting a 300s exposure image, setting flux scale and noise accordingly") noise_variance=800 # ADU^2 (Just use simple Gaussian noise here.) sky_level = 106 # ADU exp_time=300. flux_scaling = (sbit_eff_area/hst_eff_area) * exp_time *(.206/.05)**2 # Setting up a truth catalog names = [ 'gal_num', 'x_image', 'y_image', 'ra', 'dec', 'g1_meas', 'g2_meas', 'fwhm','mom_size', 'nfw_g1', 'nfw_g2', 'nfw_mu', 'redshift','flux', 'stamp_sum'] types = [ int, float, float, float, float, float, float, float, float, float, float, float, float, float, float] truth_catalog = galsim.OutputCatalog(names, types) # Set up the image: full_image = galsim.ImageF(image_xsize, image_ysize) full_image.fill(sky_level) full_image.setOrigin(0,0) # We keep track of how much noise is already in the image # This is only relevant if "RealGalaxy" type is used noise_image = galsim.ImageF(image_xsize, image_ysize) 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.0 * galsim.degrees dudx = numpy.cos(theta) * pixel_scale dudy = -numpy.sin(theta) * pixel_scale dvdx = numpy.sin(theta) * pixel_scale dvdy = numpy.cos(theta) * pixel_scale image_center = full_image.true_center affine = galsim.AffineTransform(dudx, dudy, dvdx, dvdy, origin=full_image.true_center) sky_center = galsim.CelestialCoord(ra=center_ra, dec=center_dec) wcs = galsim.TanWCS(affine, sky_center, units=galsim.arcsec) full_image.wcs = wcs # Now let's read in the PSFEx PSF model. psf_wcs=wcs psf_file = os.path.join(psf_path,psf_filen) psf = galsim.des.DES_PSFEx(psf_file,wcs=psf_wcs) logger.info('Constructed PSF object from PSFEx file') # Loop over galaxy 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) try: # make single galaxy object logger.debug("about to make stamp...") stamp,truth = make_a_galaxy(ud=ud,wcs=wcs,psf=psf,affine=affine) logger.debug("stamp is made") # Find the overlapping bounds: bounds = stamp.bounds & full_image.bounds # We need to keep track of how much variance we have currently in the image, so when # we add more noise, we can omit what is already there. # Uncomment if using "real" COSMOS galaxy type #noise_image[bounds] += truth.variance # Finally, add the stamp to the full image. full_image[bounds] += stamp[bounds] logger.debug("stamp added to full image") time2 = time.time() tot_time = time2-time1 logger.info('Galaxy %d positioned relative to center t=%f s', k, tot_time) g1_real=stamp.FindAdaptiveMom().observed_shape.g1 g2_real=stamp.FindAdaptiveMom().observed_shape.g2 sum_flux=numpy.sum(stamp.array) row = [ k,truth.x, truth.y, truth.ra, truth.dec, g1_real, g2_real, truth.fwhm, truth.mom_size, truth.g1, truth.g2, truth.mu, truth.z, truth.flux, sum_flux] truth_catalog.addRow(row) logger.debug("row added to truth catalog") except: logger.info('Galaxy %d has failed, skipping...',k) pdb.set_trace() ##### ### Inject cluster galaxy objects: ### - Note that this "cluster" is just for aesthetics ### - So, 'n_cluster_gals' is arbitrary ### - You could concievably create a method to base the number of galaxies injected ### using some scaling relation between (NFW) mass and richness to set n_cluster_gals ### to something based in reality. ##### random_seed=892375351 center_coords = galsim.CelestialCoord(center_ra,center_dec) centerpix = wcs.toImage(center_coords) n_cluster_gals = 30 for k in range(n_cluster_gals): time1 = time.time() # The usual random number generator using a different seed for each galaxy. ud = galsim.UniformDeviate(random_seed+k+1) try: # make single galaxy object cluster_stamp,truth = make_cluster_galaxy(ud=ud,wcs=wcs,affine=affine,psf=psf, centerpix=centerpix,cluster_cat=cluster_cat) # Find the overlapping bounds: bounds = cluster_stamp.bounds & full_image.bounds # We need to keep track of how much variance we have currently in the image, so when # we add more noise, we can omit what is already there. This is more relevant to # "real" galaxy images, not parametric like we have #noise_image[bounds] += truth.variance # Finally, add the stamp to the full image. full_image[bounds] += cluster_stamp[bounds] time2 = time.time() tot_time = time2-time1 logger.info('Cluster galaxy %d positioned relative to center t=%f s', k, tot_time) this_flux=numpy.sum(stamp.array) row = [ k,truth.x, truth.y, truth.ra, truth.dec, truth.g1, truth.g2, truth.mu,truth.z, this_flux] truth_catalog.addRow(row) except: logger.info('Cluster galaxy %d has failed, skipping...',k) pdb.set_trace() #### ### Now repeat process for stars! #### random_seed_stars=3221987 for k in range(nstars): time1 = time.time() ud = galsim.UniformDeviate(random_seed_stars+k+1) try: star_stamp,truth=make_a_star(ud=ud,wcs=wcs,psf=psf,affine=affine) bounds = star_stamp.bounds & full_image.bounds # Add the stamp to the full image. full_image[bounds] += star_stamp[bounds] time2 = time.time() tot_time = time2-time1 logger.info('Star %d: positioned relative to center, t=%f s', k, tot_time) g1_real=star_stamp.FindAdaptiveMom().observed_shape.g1 g2_real=star_stamp.FindAdaptiveMom().observed_shape.g2 #g1_real = -9999. #g2_real = -9999. sum_flux=numpy.sum(star_stamp.array) row = [ k,truth.x, truth.y, truth.ra, truth.dec, g1_real, g2_real, truth.fwhm, truth.mom_size, truth.g1, truth.g2, truth.mu, truth.z, truth.flux, sum_flux] truth_catalog.addRow(row) except: logger.info('Star %d has failed, skipping...',k) pdb.set_trace() # 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. #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. noise = galsim.GaussianNoise(rng, sigma=math.sqrt(noise_variance)) full_image.addNoise(noise) logger.info('Added noise to final output image') # Now write the image to disk. full_image.write(file_name) # Add a FLUXSCL keyword for later stacking this_hdu=astropy.io.fits.open(file_name) this_hdu[0].header['FLXSCALE'] = 300.0/exp_time this_hdu.writeto(file_name,overwrite='True') logger.info('Wrote image to %r',file_name) # Write truth catalog to file. truth_catalog.write(truth_file_name) i=i+1 logger.info('completed run %d for psf %s',i,psf_filen) logger.info('completed all images')
def test_scattered_whiten(): """Test whitening with the image type Scattered. In particular getting the noise flattened across overlapping stamps and stamps that are partially off the image. """ real_gal_dir = os.path.join('..', 'examples', 'data') real_gal_cat = 'real_galaxy_catalog_23.5_example.fits' scale = 0.05 index = 79 flux = 10000 variance = 10 skip_prob = 0.2 nobjects = 30 config = { 'image': { 'type': 'Scattered', 'random_seed': 12345, 'pixel_scale': scale, 'size': 100, 'image_pos': { 'type': 'XY', # Some of these will be completely off the main image. # They will be ignored. 'x': { 'type': 'Random', 'min': -50, 'max': 150 }, 'y': { 'type': 'Random', 'min': -50, 'max': 150 }, }, 'nobjects': nobjects, 'noise': { 'type': 'Gaussian', 'variance': variance, 'whiten': True, }, }, 'gal': { 'type': 'RealGalaxy', 'index': index, # It's a bit faster if they all use the same index. 'flux': flux, # This tests a special case in FlattenNoiseVariance 'skip': { 'type': 'RandomBinomial', 'p': skip_prob } }, 'psf': { 'type': 'Gaussian', 'sigma': 0.1, }, 'input': { 'real_catalog': { 'dir': real_gal_dir, 'file_name': real_gal_cat, } } } # First build by hand rgc = galsim.RealGalaxyCatalog(os.path.join(real_gal_dir, real_gal_cat)) gal = galsim.RealGalaxy(rgc, index=index, flux=flux) psf = galsim.Gaussian(sigma=0.1) final = galsim.Convolve(gal, psf) im1 = galsim.Image(100, 100, scale=scale) cv_im = galsim.Image(100, 100) for k in range(nobjects): ud = galsim.UniformDeviate(12345 + k + 1) x = ud() * 200. - 50. y = ud() * 200. - 50. skip_dev = galsim.BinomialDeviate(ud, N=1, p=skip_prob) if skip_dev() > 0: continue ix = int(math.floor(x + 1)) iy = int(math.floor(y + 1)) dx = x - ix + 0.5 dy = y - iy + 0.5 stamp = final.drawImage(offset=(dx, dy), scale=scale) stamp.setCenter(ix, iy) final.noise.rng.reset(ud) cv = final.noise.whitenImage(stamp) b = im1.bounds & stamp.bounds if not b.isDefined(): continue im1[b] += stamp[b] cv_im[b] += cv print('max cv = ', cv_im.array.max()) print('min cv = ', cv_im.array.min()) max_cv = cv_im.array.max() noise_im = max_cv - cv_im rng = galsim.BaseDeviate(12345) im1.addNoise(galsim.VariableGaussianNoise(rng, noise_im)) im1.addNoise(galsim.GaussianNoise(rng, sigma=math.sqrt(variance - max_cv))) # Compare to what config builds im2 = galsim.config.BuildImage(config) np.testing.assert_almost_equal(im2.array, im1.array) # Should give a warning for the objects that fall off the edge with CaptureLog() as cl: im3 = galsim.config.BuildImage(config, logger=cl.logger) #print(cl.output) assert "Object centered at (-24,-43) is entirely off the main image" in cl.output im2 = galsim.config.BuildImage(config)
def BuildScatteredImage(config, logger=None, image_num=0, obj_num=0, make_psf_image=False, make_weight_image=False, make_badpix_image=False): """ Build an Image containing multiple objects placed at arbitrary locations. @param config A configuration dict. @param logger If given, a logger object to log progress. [default None] @param image_num If given, the current `image_num` [default: 0] @param obj_num If given, the current `obj_num` [default: 0] @param make_psf_image Whether to make `psf_image`. [default: False] @param make_weight_image Whether to make `weight_image`. [default: False] @param make_badpix_image Whether to make `badpix_image`. [default: False] @returns the tuple `(image, psf_image, weight_image, badpix_image)`. Note: All 4 Images are always returned in the return tuple, but the latter 3 might be None depending on the parameters make_*_image. """ config['index_key'] = 'image_num' config['image_num'] = image_num config['obj_num'] = obj_num if logger: logger.debug('image %d: BuildScatteredImage: image, obj = %d,%d', image_num,image_num,obj_num) if 'random_seed' in config['image'] and not isinstance(config['image']['random_seed'],dict): first = galsim.config.ParseValue(config['image'], 'random_seed', config, int)[0] config['image']['random_seed'] = { 'type' : 'Sequence', 'first' : first } nobjects = GetNObjForScatteredImage(config,image_num) if logger: logger.debug('image %d: nobj = %d',image_num,nobjects) ignore = [ 'random_seed', 'draw_method', 'noise', 'pixel_scale', 'wcs', 'nproc', 'sky_level', 'sky_level_pixel', 'retry_failures', 'image_pos', 'world_pos', 'n_photons', 'wmult', 'offset', 'stamp_size', 'stamp_xsize', 'stamp_ysize', 'gsparams', 'nobjects' ] opt = { 'size' : int , 'xsize' : int , 'ysize' : int , 'nproc' : int , 'index_convention' : str } params = galsim.config.GetAllParams( config['image'], 'image', config, opt=opt, ignore=ignore)[0] # Special check for the size. Either size or both xsize and ysize is required. if 'size' not in params: if 'xsize' not in params or 'ysize' not in params: raise AttributeError( "Either attribute size or both xsize and ysize required for image.type=Scattered") full_xsize = params['xsize'] full_ysize = params['ysize'] else: if 'xsize' in params: raise AttributeError( "Attributes xsize is invalid if size is set for image.type=Scattered") if 'ysize' in params: raise AttributeError( "Attributes ysize is invalid if size is set for image.type=Scattered") full_xsize = params['size'] full_ysize = params['size'] # If image_force_xsize and image_force_ysize were set in config, make sure it matches. if ( ('image_force_xsize' in config and full_xsize != config['image_force_xsize']) or ('image_force_ysize' in config and full_ysize != config['image_force_ysize']) ): raise ValueError( "Unable to reconcile required image xsize and ysize with provided "+ "xsize=%d, ysize=%d, "%(full_xsize,full_ysize)) config['image_xsize'] = full_xsize config['image_ysize'] = full_ysize convention = params.get('index_convention','1') _set_image_origin(config,convention) if logger: logger.debug('image %d: image_origin = %s',image_num,str(config['image_origin'])) logger.debug('image %d: image_center = %s',image_num,str(config['image_center'])) wcs = galsim.config.BuildWCS(config, logger) # Set the rng to use for image stuff. if 'random_seed' in config['image']: # Technically obj_num+nobjects will be the index of the random seed used for the next # image's first object (if there is a next image). But I don't think that will have # any adverse effects. config['obj_num'] = obj_num + nobjects config['index_key'] = 'obj_num' seed = galsim.config.ParseValue(config['image'], 'random_seed', config, int)[0] config['index_key'] = 'image_num' if logger: logger.debug('image %d: seed = %d',image_num,seed) rng = galsim.BaseDeviate(seed) else: rng = galsim.BaseDeviate() config['rng'] = rng if 'image_pos' in config['image'] and 'world_pos' in config['image']: raise AttributeError("Both image_pos and world_pos specified for Scattered image.") if 'image_pos' not in config['image'] and 'world_pos' not in config['image']: xmin = config['image_origin'].x xmax = xmin + full_xsize-1 ymin = config['image_origin'].y ymax = ymin + full_ysize-1 config['image']['image_pos'] = { 'type' : 'XY' , 'x' : { 'type' : 'Random' , 'min' : xmin , 'max' : xmax }, 'y' : { 'type' : 'Random' , 'min' : ymin , 'max' : ymax } } nproc = params.get('nproc',1) full_image = galsim.ImageF(full_xsize, full_ysize) full_image.setOrigin(config['image_origin']) full_image.wcs = wcs full_image.setZero() if make_psf_image: full_psf_image = galsim.ImageF(full_image.bounds, wcs=wcs) full_psf_image.setZero() else: full_psf_image = None if make_weight_image: full_weight_image = galsim.ImageF(full_image.bounds, wcs=wcs) full_weight_image.setZero() else: full_weight_image = None if make_badpix_image: full_badpix_image = galsim.ImageS(full_image.bounds, wcs=wcs) full_badpix_image.setZero() else: full_badpix_image = None # Sometimes an input field needs to do something special at the start of an image. if 'input' in config: for key in [ k for k in galsim.config.valid_input_types.keys() if k in config['input'] ]: if galsim.config.valid_input_types[key][4]: assert key in config fields = config['input'][key] if not isinstance(fields, list): fields = [ fields ] input_objs = config[key] for i in range(len(fields)): field = fields[i] input_obj = input_objs[i] func = eval(galsim.config.valid_input_types[key][4]) func(input_obj, field, config) stamp_images = galsim.config.BuildStamps( nobjects=nobjects, config=config, nproc=nproc, logger=logger,obj_num=obj_num, do_noise=False, make_psf_image=make_psf_image, make_weight_image=make_weight_image, make_badpix_image=make_badpix_image) images = stamp_images[0] psf_images = stamp_images[1] weight_images = stamp_images[2] badpix_images = stamp_images[3] current_vars = stamp_images[4] max_current_var = 0. for k in range(nobjects): # This is our signal that the object was skipped. if not images[k].bounds.isDefined(): continue bounds = images[k].bounds & full_image.bounds if False: 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(images[k].bounds)) logger.debug('image %d: Overlap = %s',image_num,str(bounds)) if bounds.isDefined(): full_image[bounds] += images[k][bounds] if make_psf_image: full_psf_image[bounds] += psf_images[k][bounds] if make_weight_image: full_weight_image[bounds] += weight_images[k][bounds] if make_badpix_image: full_badpix_image[bounds] |= badpix_images[k][bounds] else: if logger: logger.warn( "Object centered at (%d,%d) is entirely off the main image,\n"%( images[k].bounds.center().x, images[k].bounds.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)) if current_vars[k] > max_current_var: max_current_var = current_vars[k] # Mark that we are no longer doing a single galaxy by deleting image_pos from config top # level, so it cannot be used for things like wcs.pixelArea(image_pos). if 'image_pos' in config: del config['image_pos'] if 'noise' in config['image']: # Apply the noise to the full image draw_method = galsim.config.GetCurrentValue(config['image'],'draw_method') if max_current_var > 0: import numpy # Then there was whitening applied in the individual stamps. # But there could be a different variance in each postage stamp, so the first # thing we need to do is bring everything up to a common level. noise_image = galsim.ImageF(full_image.bounds) for k in range(nobjects): b = images[k].bounds & full_image.bounds if b.isDefined(): noise_image[b] += current_vars[k] # Update this, since overlapping postage stamps may have led to a larger # value in some pixels. max_current_var = numpy.max(noise_image.array) # Figure out how much noise we need to add to each pixel. noise_image = max_current_var - noise_image # Add it. full_image.addNoise(galsim.VariableGaussianNoise(rng,noise_image)) # Now max_current_var is how much noise is in each pixel. config['rng'] = rng galsim.config.AddNoise( config,draw_method,full_image,full_weight_image,max_current_var,logger) else: # If we aren't doing noise, we still may need to add a non-zero sky_level. # The same noise function does this with the 'skip' draw method. galsim.config.AddNoise( config,'skip',full_image,full_weight_image,max_current_var,logger) return full_image, full_psf_image, full_weight_image, full_badpix_image
def measure_psfex_shapes(df, psfex_file, image_file, noweight, wcs, fwhm): #, logger): """Measure shapes of the PSFEx solution at each location. """ #logger.info('Read in PSFEx file: %s',psfex_file) #ignore fact that I have no star_file for now ind = df.index[df] #ind = df.index[df['star_flag'] == 1] #logger.info('ind = %s',ind) #n_psf = len(ind) #logger.info('n_psf = %s',n_psf) df['psfex_dx'] = [ -999. ] * len(df) df['psfex_dy'] = [ -999. ] * len(df) df['psfex_e1'] = [ -999. ] * len(df) df['psfex_e2'] = [ -999. ] * len(df) df['psfex_T'] = [ -999. ] * len(df) df['psfex_flux'] = [ -999. ] * len(df) df['psfex_flag'] = [ NOT_STAR ] * len(df) df.loc[ind, 'psfex_flag'] = 0 full_image = galsim.fits.read(image_file, hdu=0) if wcs is not None: full_image.wcs = wcs if not noweight: print("I'm using a weight)") full_weight = galsim.fits.read(image_file, hdu=0) full_weight.array[full_weight.array < 0] = 0. stamp_size = 48 for i in ind: x = df['X_IMAGE'].iloc[i] y = df['Y_IMAGE'].iloc[i] #print('Measure PSFEx model shape at ',x,y) image_pos = galsim.PositionD(x,y) psf_i = psf.getPSF(image_pos) b = galsim.BoundsI(int(x)-stamp_size/2, int(x)+stamp_size/2, int(y)-stamp_size/2, int(y)+stamp_size/2) b = b & full_image.bounds im = full_image[b] im = psf_i.drawImage(image=im, method='no_pixel') im *= df['obs_flux'].iloc[i] if noweight: wt = None else: wt = full_weight[b] var = wt.copy() var.invertSelf() im.addNoise(galsim.VariableGaussianNoise(rng, var)) dx, dy, e1, e2, T, flux, flag = ngmix_fit(im, wt, fwhm, x, y, logger) if np.any(np.isnan([dx,dy,e1,e2,T,flux])): logger.info(' *** NaN detected (%f,%f,%f,%f,%f,%f).',dx,dy,e1,e2,T,flux) flag |= BAD_MEASUREMENT else: df.loc[i, 'psfex_dx'] = dx df.loc[i, 'psfex_dy'] = dy df.loc[i, 'psfex_e1'] = e1 df.loc[i, 'psfex_e2'] = e2 df.loc[i, 'psfex_T'] = T df.loc[i, 'psfex_flux'] = flux df.loc[i, 'psfex_flag'] |= flag logger.info('final psfex_flag = %s',df['psfex_flag'][ind].values) #print('df[ind] = ',df.loc[ind].describe()) flag_outliers(df, ind, 'psfex', 4., logger)
cat['uIDs'] = out_uids cat['gIDs'] = out_gids cat['mags'] = out_mags cat['flux'] = out_flux cat['phtz'] = out_phtz # Export the catalog catfile = "./TianGongCosmosCat.hdf5" cat.write(catfile, path='/catalog', append=True, overwrite=True) logger.info("") # add noise logger.info(" Add noise to final large image") maxCurrentVariance = np.max(noiseImage.array) noiseImage = maxCurrentVariance - noiseImage vn = galsim.VariableGaussianNoise(rng, noiseImage) fullImage.addNoise(vn) # # noiseVariance -= maxCurrentVariance # noise = galsim.GaussianNoise(rng, sigma=np.sqrt(noiseVariance)) # fullImage.addNoise(noise) # write the image to real world fullImage.write(outImgName) logger.info(" Write image to %s", outImgName) t3 = time.time() tt = (t3 - t0)/60.0 logger.info("^_^ Total %5.2f minutes are elapsed.", tt)
def main(argv): """ Make images using model PSFs and galaxy cluster shear: - The galaxies come from COSMOSCatalog, which can produce either RealGalaxy profiles (like in demo10) and parametric fits to those profiles. We choose 40% 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("mock_superbit_data") # Define some parameters we'll use below. # Normally these would be read in from some parameter file. global pixel_scale pixel_scale = 0.206 # arcsec/pixel global image_xsize image_xsize = 6665 # size of image in pixels global image_ysize image_ysize = 4453 # size of image in pixels global image_xsize_arcsec image_xsize_arcsec = image_xsize * pixel_scale # size of big image in each dimension (arcsec) global image_ysize_arcsec image_ysize_arcsec = image_ysize * pixel_scale # size of big image in each dimension (arcsec) global center_ra center_ra = 19.3 * galsim.hours # The RA, Dec of the center of the image on the sky global center_dec center_dec = -33.1 * galsim.degrees global exp_time exp_time = 3000 # exposing for 1500 seconds to match real, observed galaxy/flux count. global noise_variance noise_variance = 1.8e3 # ADU^2 (Just use simple Gaussian noise here.) -->150s #noise_variance = 2.55e3 # ADU^2 (Just use simple Gaussian noise here.) -->300s global sky_level sky_level = 51 # ADU / arcsec^2 -->150s #sky_level = 106 # ADU / arcsec^2 -->300s global nobj nobj = 1700 # number of galaxies in entire field -- an adjustment to ensure ~1100 detections global nstars nstars = 370 # number of stars in the entire field global flux_scaling # Let's figure out the flux for a 0.5 m class telescope global tel_diam tel_diam = 0.5 global lam lam = 587 # Central wavelength psf_path = '/Users/jemcclea/Research/GalSim/examples/data/fpsc_flight_jitter_psf_oversampled_fixed_10x' global optics # will store the Zernicke component of the PSF global nfw # will store the NFWHalo information global cosmos_cat # will store the COSMOS catalog from which we draw objects # Set up the NFWHalo: mass = 5E14 # Cluster mass (Msol/h) nfw_conc = 4 # Concentration parameter = virial radius / NFW scale radius nfw_z_halo = 0.3 # redshift of the halo nfw_z_source = 0.6 # redshift of the lensed sources omega_m = 0.3 # Omega matter for the background cosmology. omega_lam = 0.7 # Omega lambda for the background cosmology. field_g1 = 0.03 # The field shear is some cosmic shear applied to the whole field, field_g2 = 0.01 # taken to be behind the foreground NFW halo (not needed for now) nfw = galsim.NFWHalo(mass=mass, conc=nfw_conc, redshift=nfw_z_halo, omega_m=omega_m, omega_lam=omega_lam) logger.info('Set up NFW halo for lensing') # Read in galaxy 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) # 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. hst_eff_area = 2.4**2 * (1. - 0.33**2) sbit_eff_area = tel_diam**2 * ( 1. - 0.10**2 ) # For want of something better, operating with 10% obscuration flux_scaling = (sbit_eff_area / hst_eff_area) * exp_time ### Now create PSF. First, define Zernicke polynomial component lam_over_diam = lam * 1.e-9 / tel_diam # radians lam_over_diam *= 206265 # arcsec aberrations = [0.0] * 12 # Set the initial size. aberrations[4] = -0.00725859 # Noll index 4 = Defocus aberrations[5:7] = [0.0, -0.00] # Noll index 5,6 = Astigmatism aberrations[7:9] = [0.07, 0.00] # Noll index 7,8 = Coma aberrations[11] = 0.00133254 # Noll index 11 = Spherical logger.info('Calculated lambda over diam = %f arcsec', lam_over_diam) optics = galsim.OpticalPSF(lam_over_diam, obscuration=0.10, aberrations=aberrations) logger.info('Made telescope PSF profile') ### ### LOOP OVER PSFs TO MAKE GROUPS OF IMAGES ### WITHIN EACH PSF, ITERATE 5 TIMES TO MAKE 5 SEPARATE IMAGES ### all_psfs = glob.glob(psf_path + "/*.psf") logger.info('Beginning loop over jitter/optical psfs') for psf_filen in all_psfs: logger.info('Beginning PSF %s...' % psf_filen) for i in numpy.arange(1, 6): logger.info('Beginning loop %d' % i) random_seed = scipy.random.randint(low=10000000, high=99999999) rng = galsim.BaseDeviate(random_seed) # This is specific to Javier mock PSFs try: root = psf_filen.split('data/')[1].split('/')[0] timescale = psf_filen.split('_10x/')[1].split('.')[0] outname = ''.join([ 'mock_superbit_', root, timescale, str(i).zfill(3), '.fits' ]) truth_file_name = ''.join([ './output/truth_', root, timescale, str(i).zfill(3), '.dat' ]) file_name = os.path.join('output', outname) except: pdb.set_trace() # Setting up a truth catalog names = [ 'gal_num', 'x_image', 'y_image', 'ra', 'dec', 'g1_meas', 'g2_meas', 'nfw_g1', 'nfw_g2', 'nfw_mu', 'redshift', 'flux' ] types = [ int, float, float, float, float, float, float, float, float, float, float, float ] truth_catalog = galsim.OutputCatalog(names, types) # Set up the image: full_image = galsim.ImageF(image_xsize, image_ysize) full_image.fill(sky_level) full_image.setOrigin(0, 0) # We keep track of how much noise is already in the image from the RealGalaxies. noise_image = galsim.ImageF(image_xsize, image_ysize) 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 = numpy.cos(theta) * pixel_scale dudy = -numpy.sin(theta) * pixel_scale dvdx = numpy.sin(theta) * pixel_scale dvdy = numpy.cos(theta) * pixel_scale image_center = full_image.true_center affine = galsim.AffineTransform(dudx, dudy, dvdx, dvdy, origin=full_image.true_center) sky_center = galsim.CelestialCoord(ra=center_ra, dec=center_dec) wcs = galsim.TanWCS(affine, sky_center, units=galsim.arcsec) full_image.wcs = wcs # Now let's read in the PSFEx PSF model. We read the image directly into an # InterpolatedImage GSObject, so we can manipulate it as needed psf_wcs = wcs psf_file = os.path.join(psf_path, psf_filen) psf = galsim.des.DES_PSFEx(psf_file, wcs=psf_wcs) logger.info('Constructed PSF object from PSFEx file') # Loop over galaxy 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) try: # make single galaxy object stamp, truth = make_a_galaxy(ud=ud, wcs=wcs, psf=psf, affine=affine) # Find the overlapping bounds: bounds = stamp.bounds & full_image.bounds # We need to keep track of how much variance we have currently in the image, so when # we add more noise, we can omit what is already there. noise_image[bounds] += truth.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 positioned relative to center t=%f s', k, tot_time) #g1_real=stamp.FindAdaptiveMom().observed_shape.g1 #g2_real=stamp.FindAdaptiveMom().observed_shape.g2 g1_real = -9999. g2_real = -9999. this_flux = numpy.sum(stamp.array) row = [ k, truth.x, truth.y, truth.ra, truth.dec, g1_real, g2_real, truth.g1, truth.g2, truth.mu, truth.z, this_flux ] truth_catalog.addRow(row) except: logger.info('Galaxy %d has failed, skipping...', k) #### ### Now repeat process for stars! #### random_seed_stars = scipy.random.randint(low=10000000, high=99999999) for k in range(nstars): time1 = time.time() ud = galsim.UniformDeviate(random_seed_stars + k + 1) star_stamp, truth = make_a_star(ud=ud, wcs=wcs, psf=psf, affine=affine) bounds = star_stamp.bounds & full_image.bounds # Add the stamp to the full image. try: full_image[bounds] += star_stamp[bounds] time2 = time.time() tot_time = time2 - time1 logger.info( 'Star %d: positioned relative to center, t=%f s', k, tot_time) #g1_real=star_stamp.FindAdaptiveMom().observed_shape.g1 --> no longer positive definite :-? #g2_real=star_stamp.FindAdaptiveMom().observed_shape.g2 g1_real = -9999. g2_real = -9999. this_flux = numpy.sum(star_stamp.array) row = [ k, truth.x, truth.y, truth.ra, truth.dec, g1_real, g2_real, truth.g1, truth.g2, truth.mu, truth.z, this_flux ] truth_catalog.addRow(row) except: logger.info('Star %d has failed, skipping...', k) # 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. 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. 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) # Write truth catalog to file. truth_catalog.write(truth_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_xsize - 1), (image_ysize - 1, 0), (image_xsize - 1, image_ysize - 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.' ) i = i + 1 logger.info(' ') logger.info('completed run %d for psf %s', i, psf_filen) logger.info('completed all images')
def main(argv): """ Make images using constant PSF and variable shear: - The main image is 0.2 x 0.2 degrees. - Pixel scale is 0.2 arcsec, hence the image is 3600 x 3600 pixels. - Applied shear is from a cosmological power spectrum read in from file. - The PSF is a real one from SDSS, and corresponds to a convolution of atmospheric PSF, optical PSF, and pixel response, which has been sampled at pixel centers. We used a PSF from SDSS in order to have a PSF profile that could correspond to what you see with a real telescope. However, in order that the galaxy resolution not be too poor, we tell GalSim that the pixel scale for that PSF image is 0.2" rather than 0.396". We are simultaneously lying about the intrinsic size of the PSF and about the pixel scale when we do this. - The galaxy images include some initial correlated noise from the original HST observation. However, we whiten the noise of the final image so the final image has stationary Gaussian noise, rather than correlated noise. """ logging.basicConfig(format="%(message)s", level=logging.INFO, stream=sys.stdout) logger = logging.getLogger("demo11") # Define some parameters we'll use below. # Normally these would be read in from some parameter file. base_stamp_size = 32 # number of pixels in each dimension of galaxy images # This will be scaled up according to the dilation. # Hence the "base_" prefix. pixel_scale = 0.2 # arcsec/pixel image_size = 0.2 * galsim.degrees # size of big image in each dimension image_size = int( (image_size / galsim.arcsec) / pixel_scale) # convert to pixels image_size_arcsec = image_size * pixel_scale # size of big image in each dimension (arcsec) noise_variance = 1.e4 # ADU^2 nobj = 288 # number of galaxies in entire field # (This corresponds to 2 galaxies / arcmin^2) grid_spacing = 90.0 # The spacing between the samples for the power spectrum # realization (arcsec) gal_signal_to_noise = 100 # S/N of each galaxy # random_seed is used for both the power spectrum realization and the random properties # of the galaxies. random_seed = 24783923 file_name = os.path.join('output', 'tabulated_power_spectrum.fits.fz') logger.info('Starting demo script 11') # Read in galaxy catalog cat_file_name = 'real_galaxy_catalog_example.fits' dir = 'data' real_galaxy_catalog = galsim.RealGalaxyCatalog(cat_file_name, dir=dir) logger.info('Read in %d real galaxies from catalog', real_galaxy_catalog.nobjects) # List of IDs to use. We select 5 particularly irregular galaxies for this demo. # Then we'll choose randomly from this list. id_list = [106416, 106731, 108402, 116045, 116448] # We will cache the galaxies that we make in order to save some of the calculations that # happen on construction. In particular, we don't want to recalculate the Fourier transforms # of the real galaxy images, so it's more efficient so make a store of RealGalaxy instances. # We start with them all = None, and fill them in as we make them. gal_list = [None] * len(id_list) # Setup the PowerSpectrum object we'll be using: # To do this, we first have to read in the tabulated shear power spectrum, often denoted # C_ell(ell), where ell has units of inverse angle and C_ell has units of angle^2. However, # GalSim works in the flat-sky approximation, so we use this notation interchangeably with # P(k). GalSim does not calculate shear power spectra for users, who must be able to provide # their own (or use the examples in the repository). # # Here we use a tabulated power spectrum from iCosmo (http://icosmo.org), with the following # cosmological parameters and survey design: # H_0 = 70 km/s/Mpc # Omega_m = 0.25 # Omega_Lambda = 0.75 # w_0 = -1.0 # w_a = 0.0 # n_s = 0.96 # sigma_8 = 0.8 # Smith et al. prescription for the non-linear power spectrum. # Eisenstein & Hu transfer function with wiggles. # Default dN/dz with z_med = 1.0 # The file has, as required, just two columns which are k and P(k). However, iCosmo works in # terms of ell and C_ell; ell is inverse radians and C_ell in radians^2. Since GalSim tends to # work in terms of arcsec, we have to tell it that the inputs are radians^-1 so it can convert # to store in terms of arcsec^-1. pk_file = os.path.join('data', 'cosmo-fid.zmed1.00.out') ps = galsim.PowerSpectrum(pk_file, units=galsim.radians) # The argument here is "e_power_function" which defines the E-mode power to use. logger.info('Set up power spectrum from tabulated P(k)') # Now let's read in the PSF. It's a real SDSS PSF, which means pixel scale of 0.396". However, # the typical seeing is 1.2" and we want to simulate better seeing, so we will just tell GalSim # that the pixel scale is 0.2". We have to be careful with SDSS PSF images, as they have an # added 'soft bias' of 1000 which has been removed before creation of this file, so that the sky # level is properly zero. Also, the file is bzipped, to demonstrate the new capability of # reading in a file that has been compressed in various ways (which GalSim can infer from the # filename). We want to read the image directly into an InterpolatedImage GSObject, so we can # manipulate it as needed (here, the only manipulation needed is convolution). We want a PSF # with flux 1, and we can set the pixel scale using a keyword. psf_file = os.path.join('data', 'example_sdss_psf_sky0.fits.bz2') psf = galsim.InterpolatedImage(psf_file, scale=pixel_scale, flux=1.) logger.info('Read in PSF image from bzipped FITS file') # Setup the image: full_image = galsim.ImageF(image_size, image_size) # The default convention for indexing an image is to follow the FITS standard where the # lower-left pixel is called (1,1). However, this can be counter-intuitive to people more # used to C or python indexing, where indices start at 0. It is possible to change the # coordinates of the lower-left pixel with the methods `setOrigin`. For this demo, we # switch to 0-based indexing, so the lower-left pixel will be called (0,0). full_image.setOrigin(0, 0) # As for demo10, we use random_seed+nobj for the random numbers required for the # whole image. In this case, both the power spectrum realization and the noise on the # full image we apply later. rng = galsim.BaseDeviate(random_seed + nobj) # We want to make random positions within our image. However, currently for shears from a power # spectrum we first have to get shears on a grid of positions, and then we can choose random # positions within that. So, let's make the grid. We're going to make it as large as the # image, with grid points spaced by 90 arcsec (hence interpolation only happens below 90" # scales, below the interesting scales on which we want the shear power spectrum to be # represented exactly). The lensing engine wants positions in arcsec, so calculate that: ps.buildGrid(grid_spacing=grid_spacing, ngrid=int(math.ceil(image_size_arcsec / grid_spacing)), rng=rng.duplicate()) logger.info('Made gridded shears') # We keep track of how much noise is already in the image from the RealGalaxies. # The default initial value is all pixels = 0. noise_image = galsim.ImageF(image_size, image_size) noise_image.setOrigin(0, 0) # Make a slightly non-trivial WCS. We'll use a slightly rotated coordinate system # and center it at the image center. theta = 0.17 * galsim.degrees # ( dudx dudy ) = ( cos(theta) -sin(theta) ) * pixel_scale # ( dvdx dvdy ) ( sin(theta) cos(theta) ) dudx = math.cos(theta.rad()) * pixel_scale dudy = -math.sin(theta.rad()) * pixel_scale dvdx = math.sin(theta.rad()) * pixel_scale dvdy = math.cos(theta.rad()) * pixel_scale image_center = full_image.trueCenter() affine = galsim.AffineTransform(dudx, dudy, dvdx, dvdy, origin=full_image.trueCenter()) # We can also put it on the celestial sphere to give it a bit more realism. # The TAN projection takes a (u,v) coordinate system on a tangent plane and projects # that plane onto the sky using a given point as the tangent point. The tangent # point should be given as a CelestialCoord. sky_center = galsim.CelestialCoord(ra=19.3 * galsim.hours, dec=-33.1 * galsim.degrees) # The third parameter, units, defaults to arcsec, but we make it explicit here. # It sets the angular units of the (u,v) intermediate coordinate system. wcs = galsim.TanWCS(affine, sky_center, units=galsim.arcsec) full_image.wcs = wcs # Now we need to loop over our objects: for k in range(nobj): time1 = time.time() # The usual random number generator using a different seed for each galaxy. ud = galsim.UniformDeviate(random_seed + k) # Draw the size from a plausible size distribution: N(r) ~ r^-2.5 # For this, we use the class DistDeviate which can draw deviates from an arbitrary # probability distribution. This distribution can be defined either as a functional # form as we do here, or as tabulated lists of x and p values, from which the # function is interpolated. # N.B. This calculation logically belongs later in the script, but given how the config # structure works and the fact that we also use this value for the stamp size # calculation, in order to get the output file to match the YAML output file, it # turns out this is where we need to put this use of the random number generator. distdev = galsim.DistDeviate(ud, function=lambda x: x**-2.5, x_min=1, x_max=5) dilat = distdev() # Choose a random position in the image x = ud() * (image_size - 1) y = ud() * (image_size - 1) image_pos = galsim.PositionD(x, y) # Turn this into a position in world coordinates # We leave this in the (u,v) plane, since the PowerSpectrum class is really defined # on the tangent plane, not in (ra,dec). world_pos = affine.toWorld(image_pos) # Get the reduced shears and magnification at this point g1, g2, mu = ps.getLensing(pos=world_pos) # Construct the galaxy: # Select randomly from among our list of galaxies. index = int(ud() * len(gal_list)) gal = gal_list[index] # If we haven't made this galaxy yet, we need to do so. if gal is None: # When whitening the image, we need to make sure the original correlated noise is # present throughout the whole image, otherwise the whitening will do the wrong thing # to the parts of the image that don't include the original image. The RealGalaxy # stores the correct noise profile to use as the gal.noise attribute. This noise # profile is automatically updated as we shear, dilate, convolve, etc. But we need to # tell it how large to pad with this noise by hand. This is a bit complicated for the # code to figure out on its own, so we have to supply the size for noise padding # with the noise_pad_size parameter. # In this case, the postage stamp will be 32 pixels for the undilated galaxies. # We expand the postage stamp as we dilate the galaxies, so that factor doesn't # come into play here. The shear and magnification are not significant, but the # image can be rotated, which adds an extra factor of sqrt(2). So the net required # padded size is # noise_pad_size = 32 * sqrt(2) * 0.2 arcsec/pixel = 9.1 arcsec # We round this up to 10 to be safe. gal = galsim.RealGalaxy(real_galaxy_catalog, rng=ud, id=id_list[index], noise_pad_size=10) # Save it for next time we use this galaxy. gal_list[index] = gal # Apply the dilation we calculated above. gal = gal.dilate(dilat) # Apply a random rotation theta = ud() * 2.0 * numpy.pi * galsim.radians gal = gal.rotate(theta) # Apply the cosmological (reduced) shear and magnification at this position using a single # GSObject method. gal = gal.lens(g1, g2, mu) # Convolve with the PSF. final = galsim.Convolve(psf, gal) # Account for the fractional part of the position: ix = int(math.floor(x + 0.5)) iy = int(math.floor(y + 0.5)) offset = galsim.PositionD(x - ix, y - iy) # Draw it with our desired stamp size (scaled up by the dilation factor): # Note: We make the stamp size odd to make the above calculation of the offset easier. this_stamp_size = 2 * int(math.ceil(base_stamp_size * dilat / 2)) + 1 stamp = galsim.ImageF(this_stamp_size, this_stamp_size) # We use method='no_pixel' here because the SDSS PSF image that we are using includes the # pixel response already. final.drawImage(image=stamp, wcs=wcs.local(image_pos), offset=offset, method='no_pixel') # Now we can whiten or symmetrize the noise on the postage stamp. Galsim automatically # propagates the noise correctly from the initial RealGalaxy object through the applied # shear, distortion, rotation, and convolution into the final object's noise attribute. To # make the noise fully white, use the image.whitenNoise() method. The returned value is the # variance of the Gaussian noise that is present after the whitening process. # # However, this is often overkill for many applications. If it is acceptable to merely end # up with noise with some degree of symmetry (say 4-fold or 8-fold symmetry), then you can # instead have GalSim just add enough noise to make the resulting noise have this kind of # symmetry. Usually this requires adding significantly less additional noise, which means # you can have the resulting total variance be somewhat smaller. The returned variance # corresponds to the zero-lag value of the noise correlation function, which will still have # off-diagonal elements. We can do this step using the image.symmetrizeNoise() method. #new_variance = stamp.whitenNoise(final.noise) new_variance = stamp.symmetrizeNoise(final.noise, 8) # Rescale flux to get the S/N we want. We have to do that before we add it to the big # image, which might have another galaxy near that point (so our S/N calculation would # erroneously include the flux from the other object). # See demo5.py for the math behind this calculation. sn_meas = math.sqrt(numpy.sum(stamp.array**2) / noise_variance) flux_scaling = gal_signal_to_noise / sn_meas stamp *= flux_scaling # This also scales up the current variance by flux_scaling**2. new_variance *= flux_scaling**2 # Recenter the stamp at the desired position: stamp.setCenter(ix, iy) # Find the overlapping bounds: bounds = stamp.bounds & full_image.bounds full_image[bounds] += stamp[bounds] # We need to keep track of how much variance we have currently in the image, so when # we add more noise, we can omit what is already there. noise_image[bounds] += new_variance time2 = time.time() tot_time = time2 - time1 logger.info('Galaxy %d: position relative to center = %s, t=%f s', k, str(world_pos), tot_time) # We already have some noise in the image, but it isn't uniform. So the first thing to do is # to make the Gaussian noise uniform across the whole image. We have a special noise class # that can do this. VariableGaussianNoise takes an image of variance values and applies # Gaussian noise with the corresponding variance to each pixel. # So all we need to do is build an image with how much noise to add to each pixel to get us # up to the maximum value that we already have in the image. max_current_variance = numpy.max(noise_image.array) noise_image = max_current_variance - noise_image vn = galsim.VariableGaussianNoise(rng, noise_image) full_image.addNoise(vn) # Now max_current_variance is the noise level across the full image. We don't want to add that # twice, so subtract off this much from the intended noise that we want to end up in the image. noise_variance -= max_current_variance # Now add Gaussian noise with this variance to the final image. We have to do this step # at the end, rather than adding to individual postage stamps, in order to get the noise # level right in the overlap regions between postage stamps. noise = galsim.GaussianNoise(rng, sigma=math.sqrt(noise_variance)) full_image.addNoise(noise) logger.info('Added noise to final large image') # Now write the image to disk. It is automatically compressed with Rice compression, # since the filename we provide ends in .fz. full_image.write(file_name) logger.info('Wrote image to %r', file_name) # Compute some sky positions of some of the pixels to compare with the values of RA, Dec # that ds9 reports. ds9 always uses (1,1) for the lower left pixel, so the pixel coordinates # of these pixels are different by 1, but you can check that the RA and Dec values are # the same as what GalSim calculates. ra_str = sky_center.ra.hms() dec_str = sky_center.dec.dms() logger.info('Center of image is at RA %sh %sm %ss, DEC %sd %sm %ss', ra_str[0:3], ra_str[3:5], ra_str[5:], dec_str[0:3], dec_str[3:5], dec_str[5:]) for (x, y) in [(0, 0), (0, image_size - 1), (image_size - 1, 0), (image_size - 1, image_size - 1)]: world_pos = wcs.toWorld(galsim.PositionD(x, y)) ra_str = world_pos.ra.hms() dec_str = world_pos.dec.dms() logger.info('Pixel (%4d, %4d) is at RA %sh %sm %ss, DEC %sd %sm %ss', x, y, ra_str[0:3], ra_str[3:5], ra_str[5:], dec_str[0:3], dec_str[3:5], dec_str[5:]) logger.info( 'ds9 reports these pixels as (1,1), (1,3600), etc. with the same RA, Dec.' )