def test_OpticalPSF_flux(): """Compare an unaberrated OpticalPSF flux to unity. """ import time t1 = time.time() lods = ( 1.e-8, 4., 9.e5 ) # lambda/D values: don't choose unity in case symmetry hides something nlook = 512 # Need a bit bigger image than below to get enough flux image = galsim.ImageF(nlook, nlook) for lod in lods: optics_test = galsim.OpticalPSF(lam_over_diam=lod, pad_factor=1) optics_array = optics_test.draw(dx=.25 * lod, image=image).array np.testing.assert_almost_equal( optics_array.sum(), 1., 2, err_msg="Unaberrated Optical flux not quite unity.") t2 = time.time() print 'time for %s = %.2f' % (funcname(), t2 - t1)
def getProfile(self, params): """Get a version of the model as a GalSim GSObject :param params: A np array with [z4, z5, z6...z11] :returns: a galsim.GSObject instance """ import galsim prof = [] # gaussian if self.sigma is not None: gaussian = galsim.Gaussian(sigma=self.sigma) prof.append(gaussian) # atmosphere if len(self.kolmogorov_kwargs) > 0: atm = galsim.Kolmogorov(**self.kolmogorov_kwargs) prof.append(atm) # optics if params is None or len(params) == 0: # no optics here pass else: aberrations = [0, 0, 0, 0] + list(params) optics = galsim.OpticalPSF(aberrations=aberrations, **self.optical_psf_kwargs) prof.append(optics) # convolve together if len(prof) == 0: raise RuntimeError('No profile returned by model!') if len(prof) == 1: prof = prof[0] else: prof = galsim.Convolve(prof) if self.g1 is not None or self.g2 is not None: prof = prof.shear(g1=self.g1, g2=self.g2) return prof
def _BuildOpticalPSF(config, base, ignore, gsparams, logger): """@brief Build an OpticalPSF. """ kwargs, safe = galsim.config.GetAllParams(config, base, req = galsim.OpticalPSF._req_params, opt = galsim.OpticalPSF._opt_params, single = galsim.OpticalPSF._single_params, ignore = [ 'aberrations' ] + ignore) if gsparams: kwargs['gsparams'] = galsim.GSParams(**gsparams) if 'aberrations' in config: aber_list = [0.0] * 4 # Initial 4 values are ignored. aberrations = config['aberrations'] if not isinstance(aberrations,list): raise AttributeError("aberrations entry for config.OpticalPSF entry is not a list.") for i in range(len(aberrations)): value, safe1 = galsim.config.ParseValue(aberrations, i, base, float) aber_list.append(value) safe = safe and safe1 kwargs['aberrations'] = aber_list return galsim.OpticalPSF(**kwargs), safe
def get_psf(self, x, y, additional_coefs=None): """ - x: x position on FOV [deg] - y: y position on FOV [deg] - additional_coefs: ndarray (Noll ordering: defocus, astig1, astig2, coma1, coma2, trefoil1, trefoil2, spher), additional Zernike coefficients. If None, no additional errors are added [wavelength] Outputs - optics: galsim.optics.OpticalPSF """ if x < self.xmin or x > self.xmax or y < self.ymin or y > self.ymax: import warnings warnings.warn("Warning: position (%f,%f) not within the bounds " % (x, y) + "of the gridded values. Min, max x: (%f,%f) " % (self.xmin, self.xmax) + "and min, max y: (%f, %f) " % (self.ymin, self.ymax)) coefs = self.get_zernike_coefficients(x, y) if additional_coefs is not None: coefs += additional_coefs optics = galsim.OpticalPSF(lam_over_diam=self.lam_over_diam, defocus=coefs[0], astig1=coefs[1], astig2=coefs[2], coma1=coefs[3], coma2=coefs[4], trefoil1=coefs[5], trefoil2=coefs[6], spher=coefs[7], obscuration=self.obscuration, nstruts=self.nstruts, strut_thick=self.strut_thick, strut_angle=self.strut_angle, pad_factor=self.pad_factor, suppress_warning=True) return optics
def test_OpticalPSF_vs_Airy_with_obs(): """Compare the array view on an unaberrated OpticalPSF with obscuration to that of an Airy. """ import time t1 = time.time() lod = 7.5 # lambda/D value: don't choose unity in case symmetry hides something obses = (0.1, 0.3, 0.5) # central obscuration radius ratios nlook = 100 # size of array region at the centre of each image to compare image = galsim.ImageF(nlook,nlook) for obs in obses: airy_test = galsim.Airy(lam_over_diam=lod, obscuration=obs, flux=1.) optics_test = galsim.OpticalPSF(lam_over_diam=lod, pad_factor=1, obscuration=obs, suppress_warning=True) airy_array = airy_test.drawImage(scale=1.,image=image, method='no_pixel').array optics_array = optics_test.drawImage(scale=1.,image=image, method='no_pixel').array np.testing.assert_array_almost_equal(optics_array, airy_array, decimal_dft, err_msg="Unaberrated Optical with obscuration not quite equal to Airy") do_pickle(optics_test, lambda x: x.drawImage(nx=20, ny=20, scale=1.7, method='no_pixel')) do_pickle(optics_test) t2 = time.time() print 'time for %s = %.2f'%(funcname(),t2-t1)
def test_OpticalPSF_flux(): """Compare an unaberrated OpticalPSF flux to unity. """ lods = ( 1.e-8, 4., 9.e5 ) # lambda/D values: don't choose unity in case symmetry hides something nlook = 512 # Need a bit bigger image than below to get enough flux image = galsim.ImageF(nlook, nlook) for lod in lods: optics_test = galsim.OpticalPSF(lam_over_diam=lod) optics_array = optics_test.drawImage(scale=.25 * lod, image=image, method='no_pixel').array np.testing.assert_almost_equal( optics_array.sum(), 1., 2, err_msg="Unaberrated Optical flux not quite unity.") do_pickle( optics_test, lambda x: x.drawImage(nx=20, ny=20, scale=1.7, method='no_pixel')) do_pickle(optics_test)
def test_OpticalPSF_vs_Airy(): """Compare the array view on an unaberrated OpticalPSF to that of an Airy. """ import time t1 = time.time() lods = ( 4.e-7, 9., 16.4 ) # lambda/D values: don't choose unity in case symmetry hides something nlook = 100 image = galsim.ImageF(nlook, nlook) for lod in lods: airy_test = galsim.Airy(lam_over_diam=lod, obscuration=0., flux=1.) optics_test = galsim.OpticalPSF( lam_over_diam=lod, pad_factor=1) #pad same as an Airy, natch! airy_array = airy_test.draw(dx=.25 * lod, image=image).array optics_array = optics_test.draw(dx=.25 * lod, image=image).array np.testing.assert_array_almost_equal( optics_array, airy_array, decimal_dft, err_msg="Unaberrated Optical not quite equal to Airy") t2 = time.time() print 'time for %s = %.2f' % (funcname(), t2 - t1)
def _make_psf(): jitter_fwhm = 0.3 jitter = galsim.Gaussian(flux=1., fwhm=jitter_fwhm) lam_over_diam = 0.257831 # units of arcsec aberrations = np.zeros(38) # Set the initial size. aberrations[0] = 0. # First entry must be zero aberrations[1] = -0.00305127 aberrations[4] = -0.02474205 # Noll index 4 = Defocus aberrations[11] = -0.01544329 # Noll index 11 = Spherical aberrations[22] = 0.00199235 aberrations[26] = 0.00000017 aberrations[37] = 0.00000004 optics = galsim.OpticalPSF(lam=625, diam=0.5, obscuration=0.38, nstruts=4, strut_angle=(90 * galsim.degrees), strut_thick=0.087, aberrations=aberrations) psf = galsim.Convolve([jitter, optics]) return psf
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 chose parametric fits since these are required for chromatic galaxies (ones with filter response included) - The real galaxy images include some initial correlated noise from the original HST observation, which would need to be whitened. But we are using parametric galaxies, so this isn't a concern. """ global logger logging.basicConfig(format="%(message)s", level=logging.INFO, stream=sys.stdout) logger = logging.getLogger("mock_superbit_data") M = MPIHelper() # Define some parameters we'll use below. sbparams = SuperBITParameters(argv=argv) # Set up the NFWHalo: nfw = galsim.NFWHalo(mass=sbparams.mass, conc=sbparams.nfw_conc, redshift=sbparams.nfw_z_halo, omega_m=sbparams.omega_m, omega_lam=sbparams.omega_lam) logger.info('Set up NFW halo for lensing') # Read in galaxy catalog, as well as catalog containing # information from COSMOS fits like redshifts, hlr, etc. cosmos_cat = galsim.COSMOSCatalog(sbparams.cat_file_name, dir=sbparams.cosmosdir) fitcat = Table.read( os.path.join(os.path.join(sbparams.cosmosdir, sbparams.fit_file_name))) logger.info('Read in %d galaxies from catalog and associated fit info', cosmos_cat.nobjects) cluster_cat = galsim.COSMOSCatalog(sbparams.cluster_cat_name) print('Read in %d cluster galaxies from catalog' % cosmos_cat.nobjects) ### Now create PSF. First, define Zernicke polynomial component ### note: aberrations were definined for lam = 550, and close to the ### center of the camera. The PSF degrades at the edge of the FOV lam_over_diam = sbparams.lam * 1.e-9 / sbparams.tel_diam # radians lam_over_diam *= 206265. aberrations = numpy.zeros(38) # Set the initial size. aberrations[0] = 0. # First entry must be zero aberrations[1] = -0.00305127 aberrations[4] = -0.02474205 # Noll index 4 = Defocus aberrations[11] = -0.01544329 # Noll index 11 = Spherical aberrations[22] = 0.00199235 aberrations[26] = 0.00000017 aberrations[37] = 0.00000004 logger.info('Calculated lambda over diam = %f arcsec', lam_over_diam) # will store the Zernicke component of the PSF optics = galsim.OpticalPSF(lam=sbparams.lam, diam=sbparams.tel_diam, obscuration=sbparams.obscuration, nstruts=sbparams.nstruts, strut_angle=sbparams.strut_angle, strut_thick=sbparams.strut_thick, aberrations=aberrations) logger.info('Made telescope PSF profile') # load SuperBIT bandpass bandpass = galsim.Bandpass(sbparams.bp_file, wave_type='nm', blue_limit=310, red_limit=1100) ### ### LOOP OVER PSFs TO MAKE GROUPS OF IMAGES ### WITHIN EACH PSF, ITERATE n TIMES TO MAKE n SEPARATE IMAGES ### #all_psfs=glob.glob(sbparams.psf_path+"/*121*.psf") logger.info('Beginning loop over jitter/optical psfs') for im in np.arange(1): for i in numpy.arange(1, sbparams.nexp + 1): # get MPI processes in sync at start of each image M.barrier() logger.info('Beginning loop %d' % i) #rng = galsim.BaseDeviate(sbparams.noise_seed+i) try: timescale = str(sbparams.exp_time) outname = ''.join( ['superbit_gaussPSF_', str(i).zfill(3), '.fits']) truth_file_name = ''.join([ sbparams.outdir, '/truth_gaussPSF_', str(i).zfill(3), '.dat' ]) file_name = os.path.join(sbparams.outdir, outname) except galsim.errors.GalSimError: print("naming failed, check path") 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', 'truth_fwhm', 'truth_mom' ] 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(sbparams.image_xsize, sbparams.image_ysize) sky_level = sbparams.exp_time * sbparams.sky_bkg # fill with sky_level moved until after MPI results summed 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(sbparams.image_xsize, sbparams.image_ysize) noise_image.setOrigin(0, 0) # If you wanted to make a non-trivial WCS system, could set theta to a non-zero number theta = 0.0 * galsim.degrees dudx = numpy.cos(theta) * sbparams.pixel_scale dudy = -numpy.sin(theta) * sbparams.pixel_scale dvdx = numpy.sin(theta) * sbparams.pixel_scale dvdy = numpy.cos(theta) * sbparams.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=sbparams.center_ra, dec=sbparams.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 = galsim.des.DES_PSFEx(psf_filen,wcs=psf_wcs) logger.info('Constructed PSF object from PSFEx file') ##### ## Loop over galaxy objects: ##### # get local range to iterate over in this process local_start, local_end = M.mpi_local_range(sbparams.nobj) for k in range(local_start, local_end): time1 = time.time() # The usual random number generator using a different seed for each galaxy. ud = galsim.UniformDeviate(sbparams.galobj_seed + k + 1) try: # make single galaxy object stamp, truth = make_a_galaxy(ud=ud, wcs=wcs, affine=affine, fitcat=fitcat, cosmos_cat=cosmos_cat, optics=optics, nfw=nfw, bandpass=bandpass, sbparams=sbparams) # 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.fwhm, truth.mom_size ] truth_catalog.addRow(row) except galsim.errors.GalSimError: logger.info('Galaxy %d has failed, skipping...', k) ##### ### Inject cluster galaxy objects: ##### center_coords = galsim.CelestialCoord(sbparams.center_ra, sbparams.center_dec) centerpix = wcs.toImage(center_coords) # get local range to iterate over in this process local_start, local_end = M.mpi_local_range(sbparams.nclustergal) for k in range(local_start, local_end): time1 = time.time() # The usual random number generator using a different seed for each galaxy. ud = galsim.UniformDeviate(sbparams.cluster_seed + k + 1) try: # make single galaxy object cluster_stamp, truth = make_cluster_galaxy( ud=ud, wcs=wcs, affine=affine, centerpix=centerpix, cluster_cat=cluster_cat, optics=optics, bandpass=bandpass, sbparams=sbparams) # 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.fwhm, truth.mom_size ] truth_catalog.addRow(row) except galsim.errors.GalSimError: logger.info('Cluster galaxy %d has failed, skipping...', k) ##### ### Now repeat process for stars! ##### # get local range to iterate over in this process local_start, local_end = M.mpi_local_range(sbparams.nstars) for k in range(local_start, local_end): time1 = time.time() ud = galsim.UniformDeviate(sbparams.stars_seed + k + 1) star_stamp, truth = make_a_star(ud=ud, wcs=wcs, affine=affine, optics=optics, sbparams=sbparams) 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.fwhm, truth.mom_size ] truth_catalog.addRow(row) except galsim.errors.GalSimError: logger.info('Star %d has failed, skipping...', k) # Gather results from MPI processes, reduce to single result on root # Using same names on left and right sides is hiding lots of MPI magic full_image = M.gather(full_image) truth_catalog = M.gather(truth_catalog) #noise_image = M.gather(noise_image) if M.is_mpi_root(): full_image = reduce(combine_images, full_image) truth_catalog = reduce(combine_catalogs, truth_catalog) #noise_image = reduce(combine_images, noise_image) else: # do the adding of noise and writing to disk entirely on root # root and the rest meet again at barrier at start of loop continue # The first thing to do is to make the Gaussian noise uniform across the whole image. # 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. # max_current_variance = numpy.max(noise_image.array) # noise_image = max_current_variance - noise_image # The first thing to do is to make the Gaussian noise uniform across the whole image. # Add dark current logger.info('Adding Dark current') dark_noise = sbparams.dark_current * sbparams.exp_time # np.random.normal( # sbparams.dark_current, sbparams.dark_current_std, # size=(sbparams.image_ysize, sbparams.image_xsize)) * sbparams.exp_time # dark_noise = np.clip(dark_noise, a_min=0, a_max=2**16) full_image += dark_noise # Add ccd noise; removed rng in noise logger.info('Adding CCD noise') noise = galsim.CCDNoise(sky_level=0, gain=1 / sbparams.gain, read_noise=sbparams.read_noise) full_image.addNoise(noise) logger.debug('Added noise to final output image') if not os.path.exists(os.path.dirname(file_name)): os.makedirs(os.path.dirname(file_name)) 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', im) i = i + 1 logger.info(' ') logger.info(' ') logger.info('completed all images') logger.info(' ')
cn.setVariance(1000.) # Again chosen to be non-unity # Define a PSF with which to convolve the noise field, one WITHOUT 2-fold rotational symmetry # (see test_autocorrelate in test_SBProfile.py for more info as to why this is relevant) # Make a relatively realistic mockup of a GREAT3 target image lam_over_diam_cosmos = (814.e-9 / 2.4) * (180. / np.pi) * 3600. # ~lamda/D in arcsec lam_over_diam_ground = lam_over_diam_cosmos * 2.4 / 4. # Generic 4m at same lambda psf_cosmos = galsim.Convolve([ galsim.Airy(lam_over_diam=lam_over_diam_cosmos, obscuration=0.4), galsim.Pixel(0.05) ]) psf_ground = galsim.Convolve([ galsim.Kolmogorov(fwhm=0.8), galsim.Pixel(0.18), galsim.OpticalPSF(lam_over_diam=lam_over_diam_ground, coma2=0.4, defocus=-0.6) ]) psf_shera = galsim.Convolve([ psf_ground, (galsim.Deconvolve(psf_cosmos)).createSheared(g1=0.03, g2=-0.01) ]) # Then define the convolved cosmos correlated noise model conv_cn = cn.copy() conv_cn.convolveWith(psf_shera) # Then draw the correlation function for this correlated noise as the reference refim = galsim.ImageD(smallim_size, smallim_size) conv_cn.draw(refim, dx=0.18) # Now start the tests... # # First we generate a COSMOS noise field (cosimage), read it into an InterpolatedImage and
def build_file(seed, file_name, mass, nobj, rng, truth_file_name, halo_id, first_obj_id): """A function that does all the work to build a single file. Returns the total time taken. """ t1 = time.time() # Build the image onto which we will draw the galaxies. full_image = galsim.ImageF(image_size, image_size) # The "true" center of the image is allowed to be halfway between two pixels, as is the # case for even-sized images. full_image.bounds.center() is an integer position, # which would be 1/2 pixel up and to the right of the true center in this case. im_center = full_image.bounds.trueCenter() # For the WCS, this time we use UVFunction, which lets you define arbitrary u(x,y) # and v(x,y) functions. We use a simple cubic radial function to create a # pincushion distortion. This is a typical kind of telescope distortion, although # we exaggerate the magnitude of the effect to make it more apparent. # The pixel size in the center of the image is 0.05, but near the corners (r=362), # the pixel size is approximately 0.075, which is much more distortion than is # normally present in typical telescopes. But it makes the effect of the variable # pixel area obvious when you look at the weight image in the output files. ufunc1 = lambda x, y: 0.05 * x * (1. + 2.e-6 * (x**2 + y**2)) vfunc1 = lambda x, y: 0.05 * y * (1. + 2.e-6 * (x**2 + y**2)) # It's not required to provide the inverse functions. However, if we don't, then # you will only be able to do toWorld operations, not the inverse toImage. # The inverse function does not have to be exact either. For example, you could provide # a function that does some kind of iterative solution to whatever accuracy you care # about. But in this case, we can do the exact inverse. # # Let w = sqrt(u**2 + v**2) and r = sqrt(x**2 + y**2). Then the solutions are: # x = (u/w) r and y = (u/w) r, and we use Cardano's method to solve for r given w: # See http://en.wikipedia.org/wiki/Cubic_function#Cardano.27s_method # # w = 0.05 r + 2.e-6 * 0.05 * r**3 # r = 100 * ( ( 5 sqrt(w**2 + 5.e3/27) + 5 w )**(1./3.) - # - ( 5 sqrt(w**2 + 5.e3/27) - 5 w )**(1./3.) ) def xfunc1(u, v): import math wsq = u * u + v * v if wsq == 0.: return 0. else: w = math.sqrt(wsq) temp = 5. * math.sqrt(wsq + 5.e3 / 27) r = 100. * ((temp + 5 * w)**(1. / 3.) - (temp - 5 * w)**(1. / 3)) return u * r / w def yfunc1(u, v): import math wsq = u * u + v * v if wsq == 0.: return 0. else: w = math.sqrt(wsq) temp = 5. * math.sqrt(wsq + 5.e3 / 27) r = 100. * ((temp + 5 * w)**(1. / 3.) - (temp - 5 * w)**(1. / 3)) return v * r / w # You could pass the above functions to UVFunction, and normally we would do that. # The only down side to doing so is that the specification of the WCS in the FITS # file is rather ugly. GalSim is able to turn the python byte code into strings, # but they are basically a really ugly mess of random-looking characters. GalSim # will be able to read it back in, but human readers will have no idea what WCS # function was used. To see what they look like, uncomment this line and comment # out the later wcs line. #wcs = galsim.UVFunction(ufunc1, vfunc1, xfunc1, yfunc1, origin=im_center) # If you provide the functions as strings, then those strings will be preserved # in the FITS header in a form that is more legible to human readers. # It also has the extra benefit of matching the output from demo9.yaml, which we # always try to do. The config file has no choice but to specify the functions # as strings. ufunc = '0.05 * x * (1. + 2.e-6 * (x**2 + y**2))' vfunc = '0.05 * y * (1. + 2.e-6 * (x**2 + y**2))' xfunc = ( '( lambda w: ( 0 if w==0 else ' + '100.*u/w*(( 5*(w**2 + 5.e3/27.)**0.5 + 5*w )**(1./3.) - ' + '( 5*(w**2 + 5.e3/27.)**0.5 - 5*w )**(1./3.))))( (u**2+v**2)**0.5 )' ) yfunc = ( '( lambda w: ( 0 if w==0 else ' + '100.*v/w*(( 5*(w**2 + 5.e3/27.)**0.5 + 5*w )**(1./3.) - ' + '( 5*(w**2 + 5.e3/27.)**0.5 - 5*w )**(1./3.))))( (u**2+v**2)**0.5 )' ) # The origin parameter defines where on the image should be considered (x,y) = (0,0) # in the WCS functions. wcs = galsim.UVFunction(ufunc, vfunc, xfunc, yfunc, origin=im_center) # Assign this wcs to full_image full_image.wcs = wcs # The weight image will hold the inverse variance for each pixel. # We can set the wcs directly on construction with the wcs parameter. weight_image = galsim.ImageF(image_size, image_size, wcs=wcs) # It is common for astrometric images to also have a bad pixel mask. We don't have any # defect simulation currently, so our bad pixel masks are currently all zeros. # But someday, we plan to add defect functionality to GalSim, at which point, we'll # be able to mark those defects on a bad pixel mask. # Note: the S in ImageS means to use "short int" for the data type. # This is a typical choice for a bad pixel image. badpix_image = galsim.ImageS(image_size, image_size, wcs=wcs) # We also draw a PSF image at the location of every galaxy. This isn't normally done, # and since some of the PSFs overlap, it's not necessarily so useful to have this kind # of image. But in this case, it's fun to look at the psf image, especially with # something like log scaling in ds9 to see how crazy an aberrated OpticalPSF with # struts can look when there is no atmospheric component to blur it out. psf_image = galsim.ImageF(image_size, image_size, wcs=wcs) # We will also write some truth information to an output catalog. # In real simulations, it is often useful to have a catalog of the truth values # to compare to measurements either directly or as cuts on the galaxy sample to # find where systematic errors are largest. # For now, we just make an empty OutputCatalog object with the names and types of the # columns. names = [ 'object_id', 'halo_id', 'flux', 'radius', 'h_over_r', 'inclination.rad', 'theta.rad', 'mu', 'redshift', 'shear.g1', 'shear.g2', 'pos.x', 'pos.y', 'image_pos.x', 'image_pos.y', 'halo_mass', 'halo_conc', 'halo_redshift' ] types = [ int, int, float, float, float, float, float, float, float, float, float, float, float, float, float, float, float, float ] truth_cat = galsim.OutputCatalog(names, types) # Setup the NFWHalo stuff: nfw = galsim.NFWHalo(mass=mass, conc=nfw_conc, redshift=nfw_z_halo, omega_m=omega_m, omega_lam=omega_lam) # Note: the last two are optional. If they are omitted, then (omega_m=0.3, omega_lam=0.7) # are actually the defaults. If you only specify one of them, the other is set so that # the total is 1. But you can define both values so that the total is not 1 if you want. # Radiation is assumed to be zero and dark energy equation of state w = -1. # If you want to include either radiation or more complicated dark energy models, # you can define your own cosmology class that defines the functions a(z), E(a), and # Da(z_source, z_lens). Then you can pass this to NFWHalo as a `cosmo` parameter. # Make the PSF profile outside the loop to minimize the (significant) OpticalPSF # construction overhead. psf = galsim.OpticalPSF(lam=psf_lam, diam=psf_D, obscuration=psf_obsc, nstruts=psf_nstruts, strut_thick=psf_strut_thick, strut_angle=psf_strut_angle, defocus=psf_defocus, astig1=psf_astig1, astig2=psf_astig2, coma1=psf_coma1, coma2=psf_coma2, trefoil1=psf_trefoil1, trefoil2=psf_trefoil2) for k in range(nobj): # Initialize the random number generator we will be using for this object: ud = galsim.UniformDeviate(seed + k + 1) # Determine where this object is going to go. # We choose points randomly within a donut centered at the center of the main image # in order to avoid placing galaxies too close to the halo center where the lensing # is not weak. We use an inner radius of 3 arcsec and an outer radius of 12 arcsec, # which takes us essentially to the edge of the image. radius = 12 inner_radius = 3 max_rsq = radius**2 min_rsq = inner_radius**2 while True: # (This is essentially a do..while loop.) x = (2. * ud() - 1) * radius y = (2. * ud() - 1) * radius rsq = x**2 + y**2 if rsq >= min_rsq and rsq <= max_rsq: break pos = galsim.PositionD(x, y) # We also need the position in pixels to determine where to place the postage # stamp on the full image. image_pos = wcs.toImage(pos) # For even-sized postage stamps, the nominal center (returned by stamp.bounds.center()) # cannot be at the true center (returned by stamp.bounds.trueCenter()) of the postage # stamp, since the nominal center values have to be integers. Thus, the nominal center # is 1/2 pixel up and to the right of the true center. # If we used odd-sized postage stamps, we wouldn't need to do this. x_nominal = image_pos.x + 0.5 y_nominal = image_pos.y + 0.5 # Get the integer values of these which will be the actual nominal center of the # postage stamp image. ix_nominal = int(math.floor(x_nominal + 0.5)) iy_nominal = int(math.floor(y_nominal + 0.5)) # The remainder will be accounted for in an offset when we draw. dx = x_nominal - ix_nominal dy = y_nominal - iy_nominal offset = galsim.PositionD(dx, dy) # Draw the flux from a power law distribution: N(f) ~ f^-1.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. flux_dist = galsim.DistDeviate(ud, function=lambda x: x**-1.5, x_min=gal_flux_min, x_max=gal_flux_max) flux = flux_dist() # We introduce here another surface brightness profile, called InclinedExponential. # It represents a typical 3D galaxy disk profile inclined at an arbitrary angle # relative to face on. # # inclination = 0 degrees corresponds to a face-on disk, which is equivalent to # the regular Exponential profile. # inclination = 90 degrees corresponds to an edge-on disk. # # A random orientation corresponds to the inclination angle taking the probability # distribution: # # P(inc) = 0.5 sin(inc) # # so we again use a DistDeviate to generate these values. inc_dist = galsim.DistDeviate(ud, function=lambda x: 0.5 * math.sin(x), x_min=0, x_max=math.pi) inclination = inc_dist() * galsim.radians # The parameters scale_radius and scale_height give the scale distances in the # 3D distribution: # # I(R,z) = I_0 / (2 scale_height) * sech^2(z/scale_height) * exp(-r/scale_radius) # # These values can be given separately if desired. However, it is often easier to # give the ratio scale_h_over_r as an independent value, since the radius and height # values are correlated, while h/r is approximately independent of h or r. h_over_r = ud() * (gal_h_over_r_max - gal_h_over_r_min) + gal_h_over_r_min radius = ud() * (gal_r_max - gal_r_min) + gal_r_min # The inclination is around the x-axis, so we want to rotate the galaxy by a # random angle. theta = ud() * math.pi * 2. * galsim.radians # Make the galaxy profile with these values: gal = galsim.InclinedExponential(scale_radius=radius, scale_h_over_r=h_over_r, inclination=inclination, flux=flux) gal = gal.rotate(theta) # Now apply the appropriate lensing effects for this position from # the NFW halo mass. try: g1, g2 = nfw.getShear(pos, nfw_z_source) nfw_shear = galsim.Shear(g1=g1, g2=g2) except: # This shouldn't happen, since we exclude the inner 10 arcsec, but it's a # good idea to use the try/except block here anyway. import warnings warnings.warn( "Warning: NFWHalo shear is invalid -- probably strong lensing! " + "Using shear = 0.") nfw_shear = galsim.Shear(g1=0, g2=0) nfw_mu = nfw.getMagnification(pos, nfw_z_source) if nfw_mu < 0: import warnings warnings.warn( "Warning: mu < 0 means strong lensing! Using mu=25.") nfw_mu = 25 elif nfw_mu > 25: import warnings warnings.warn( "Warning: mu > 25 means strong lensing! Using mu=25.") nfw_mu = 25 # Calculate the total shear to apply # Since shear addition is not commutative, it is worth pointing out that # the order is in the sense that the second shear is applied first, and then # the first shear. i.e. The field shear is taken to be behind the cluster. # Kind of a cosmic shear contribution between the source and the cluster. # However, this is not quite the same thing as doing: # gal.shear(field_shear).shear(nfw_shear) # since the shear addition ignores the rotation that would occur when doing the # above lines. This is normally ok, because the rotation is not observable, but # it is worth keeping in mind. total_shear = nfw_shear + field_shear # Apply the magnification and shear to the galaxy gal = gal.magnify(nfw_mu) gal = gal.shear(total_shear) # Build the final object final = galsim.Convolve([psf, gal]) # Draw the stamp image # To draw the image at a position other than the center of the image, you can # use the offset parameter, which applies an offset in pixels relative to the # center of the image. # We also need to provide the local wcs at the current position. local_wcs = wcs.local(image_pos) stamp = final.drawImage(wcs=local_wcs, offset=offset) # Recenter the stamp at the desired position: stamp.setCenter(ix_nominal, iy_nominal) # Find overlapping bounds bounds = stamp.bounds & full_image.bounds full_image[bounds] += stamp[bounds] # Also draw the PSF psf_stamp = galsim.ImageF( stamp.bounds) # Use same bounds as galaxy stamp psf.drawImage(psf_stamp, wcs=local_wcs, offset=offset) psf_image[bounds] += psf_stamp[bounds] # Add the truth information for this object to the truth catalog row = ((first_obj_id + k), halo_id, flux, radius, h_over_r, inclination.rad(), theta.rad(), nfw_mu, nfw_z_source, total_shear.g1, total_shear.g2, pos.x, pos.y, image_pos.x, image_pos.y, mass, nfw_conc, nfw_z_halo) truth_cat.addRow(row) # Add Poisson noise to the full image # Note: The normal calculation of Poission noise isn't quite correct right now. # The pixel area is variable, which means the amount of sky flux that enters each # pixel is also variable. The wcs classes have a function `makeSkyImage` which # will fill an image with the correct amount of sky flux given the sky level # in units of ADU/arcsec^2. We use the weight image as our work space for this. wcs.makeSkyImage(weight_image, sky_level) # Add this to the current full_image (temporarily). full_image += weight_image # Add Poisson noise, given the current full_image. # The config parser uses a different random number generator for file-level and # image-level values than for the individual objects. This makes it easier to # parallelize the calculation if desired. In fact, this is why we've been adding 1 # to each seed value all along. The seeds for the objects take the values # random_seed+1 .. random_seed+nobj. The seed for the image is just random_seed, # which we built already (below) when we calculated how many objects need to # be in each file. Use the same rng again here, since this is also at image scope. full_image.addNoise(galsim.PoissonNoise(rng)) # Subtract the sky back off. full_image -= weight_image # The weight image is nominally the inverse variance of the pixel noise. However, it is # common to exclude the Poisson noise from the objects themselves and only include the # noise from the sky photons. The variance of the noise is just the sky level, which is # what is currently in the weight_image. (If we wanted to include the variance from the # objects too, then we could use the full_image before we added the PoissonNoise to it.) # So all we need to do now is to invert the values in weight_image. weight_image.invertSelf() # Write the file to disk: galsim.fits.writeMulti( [full_image, badpix_image, weight_image, psf_image], file_name) # And write the truth catalog file truth_cat.write(truth_file_name) t2 = time.time() return t2 - t1
# Gaussian # no noise #logger.info("First loop: gaussian, no noise") gal_flux = flux_dict[lam] * 2.512**(m_zero - m_gal) #gal= galsim.Convolve (galsim.Gaussian(flux=gal_flux, sigma=gal_sigma).shear(galsim.Shear(e1=e[0],e2=e[1])) , galsim.Pixel(pixel_scale), gsparams=big_fft_params ) #measurement_function_NL (gal, e1_inter_vec=gauss_no_noise[lam][e][0], e2_inter_vec=gauss_no_noise[lam][e][1], size_inter_vec=gauss_no_noise[lam][e][2], noise=None, beta=beta, string='Gausian, no noise') ###### noise #measurement_function_NL (gal, e1_inter_vec=gauss_noise[m_gal][0], e2_inter_vec=gauss_noise[m_gal][1], size_inter_vec=gauss_noise[m_gal][2], noise=noise, beta=beta, string='Gaussian, noise') #######################Optical logger.info("Third loop: Optical, no noise") gal = galsim.Convolve(galsim.OpticalPSF( lam_over_diam, obscuration=obscuration_optical, flux=gal_flux).shear(galsim.Shear(e1=e[0], e2=e[1])), galsim.Pixel(pixel_scale), gsparams=big_fft_params) measurement_function_NL( gal, e1_inter_vec=optical_no_noise[lam][e][0], e2_inter_vec=optical_no_noise[lam][e][1], size_inter_vec=optical_no_noise[lam][e][2], noise=None, beta=beta, string='Optical, no noise') ###### noise #measurement_function_NL (gal, e1_inter_vec=optical_noise[m_gal][0], e2_inter_vec=optical_noise[m_gal][1], size_inter_vec=optical_noise[m_gal][2], noise=noise, beta=beta, string='Optical, noise')
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 a fits image cube where each frame has two images of the same galaxy drawn with regular FFT convolution and with photon shooting. We do this for 5 different PSFs and 5 different galaxies, each with 4 different (random) fluxes, sizes, and shapes. """ logging.basicConfig(format="%(message)s", level=logging.INFO, stream=sys.stdout) logger = logging.getLogger("demo7") # To turn off logging: #logger.propagate = False # To turn on the debugging messages: #logger.setLevel(logging.DEBUG) # Define some parameters we'll use below. # Make output directory if not already present. if not os.path.isdir('output'): os.mkdir('output') file_name = os.path.join('output', 'cube_phot.fits.gz') random_seed = 553728 sky_level = 1.e4 # ADU / arcsec^2 pixel_scale = 0.28 # arcsec nx = 64 ny = 64 gal_flux_min = 1.e4 # Range for galaxy flux gal_flux_max = 1.e5 gal_hlr_min = 0.3 # arcsec gal_hlr_max = 1.3 # arcsec gal_e_min = 0. # Range for ellipticity gal_e_max = 0.8 psf_fwhm = 0.65 # arcsec # This script is set up as a comparison between using FFTs for doing the convolutions and # shooting photons. The two methods have trade-offs in speed and accuracy which vary # with the kind of profile being drawn and the S/N of the object, among other factors. # In addition, for each method, there are a number of parameters GalSim uses that control # aspects of the calculation that further affect the speed and accuracy. # # We encapsulate these parameters with an object called GSParams. The default values # are intended to be accurate enough for normal precision shear tests, without sacrificing # too much speed. # # Any PSF or galaxy object can be given a gsparams argument on construction that can # have different values to make the calculation more or less accurate (typically trading # off for speed or memory). # # In this script, we adjust some of the values slightly, just to show you how it works. # You could play around with these values and see what effect they have on the drawn images. # Usually, it requires a pretty drastic change in these parameters for you to be able to # notice the difference by eye. But subtle effects that may impact the shapes of galaxies # can happen well before then. # Type help(galsim.GSParams) for the complete list of parameters and more detailed # documentation, including the default values for each parameter. gsparams = galsim.GSParams( folding_threshold= 1.e-2, # maximum fractional flux that may be folded around edge of FFT maxk_threshold= 2.e-3, # k-values less than this may be excluded off edge of FFT xvalue_accuracy= 1.e-4, # approximations in real space aim to be this accurate kvalue_accuracy= 1.e-4, # approximations in fourier space aim to be this accurate shoot_accuracy= 1.e-4, # approximations in photon shooting aim to be this accurate minimum_fft_size=64) # minimum size of ffts logger.info('Starting demo script 7') # Make the PSF profiles: psf1 = galsim.Gaussian(fwhm=psf_fwhm, gsparams=gsparams) psf2 = galsim.Moffat(fwhm=psf_fwhm, beta=2.4, gsparams=gsparams) psf3_inner = galsim.Gaussian(fwhm=psf_fwhm, flux=0.8, gsparams=gsparams) psf3_outer = galsim.Gaussian(fwhm=2 * psf_fwhm, flux=0.2, gsparams=gsparams) psf3 = psf3_inner + psf3_outer atmos = galsim.Gaussian(fwhm=psf_fwhm, gsparams=gsparams) # The OpticalPSF and set of Zernike values chosen below correspond to a reasonably well aligned, # smallish ~0.3m / 12 inch diameter telescope with a central obscuration of ~0.12m or 5 inches # diameter, being used in optical wavebands. # In the Noll convention, the value of the Zernike coefficient also gives the RMS optical path # difference across a circular pupil. An RMS difference of ~0.5 or larger indicates that parts # of the wavefront are in fully destructive interference, and so we might expect aberrations to # become strong when Zernike aberrations summed in quadrature approach 0.5 wave. # The aberrations chosen in this case correspond to operating close to a 0.25 wave RMS optical # path difference. Unlike in demo3, we specify the aberrations by making a list that we pass # in using the 'aberrations' kwarg. The order of aberrations starting from index 4 is defocus, # astig1, astig2, coma1, coma2, trefoil1, trefoil2, spher as in the Noll convention. # We ignore the first 4 values so that the index number corresponds to the Zernike index # in the Noll convention. This will be particularly convenient once we start allowing # coefficients beyond spherical (index 11). c.f. The Wikipedia page about the Noll indices: # # http://en.wikipedia.org/wiki/Zernike_polynomials#Zernike_polynomials aberrations = [0.0] * 12 # Set the initial size. aberrations[4] = 0.06 # Noll index 4 = Defocus aberrations[5:7] = [0.12, -0.08] # Noll index 5,6 = Astigmatism aberrations[7:9] = [0.07, 0.04] # Noll index 7,8 = Coma aberrations[11] = -0.13 # Noll index 11 = Spherical # You could also define these all at once if that is more convenient: #aberrations = [0.0, 0.0, 0.0, 0.0, 0.06, 0.12, -0.08, 0.07, 0.04, 0.0, 0.0, -0.13] optics = galsim.OpticalPSF(lam_over_diam=0.6 * psf_fwhm, obscuration=0.4, aberrations=aberrations, gsparams=gsparams) psf4 = galsim.Convolve([atmos, optics ]) # Convolve inherits the gsparams from the first # item in the list. (Or you can supply a gsparams # argument explicitly if you want to override this.) atmos = galsim.Kolmogorov(fwhm=psf_fwhm, gsparams=gsparams) optics = galsim.Airy(lam_over_diam=0.3 * psf_fwhm, gsparams=gsparams) psf5 = galsim.Convolve([atmos, optics]) psfs = [psf1, psf2, psf3, psf4, psf5] psf_names = [ "Gaussian", "Moffat", "Double Gaussian", "OpticalPSF", "Kolmogorov * Airy" ] psf_times = [0, 0, 0, 0, 0] psf_fft_times = [0, 0, 0, 0, 0] psf_phot_times = [0, 0, 0, 0, 0] # Make the galaxy profiles: gal1 = galsim.Gaussian(half_light_radius=1, gsparams=gsparams) gal2 = galsim.Exponential(half_light_radius=1, gsparams=gsparams) gal3 = galsim.DeVaucouleurs(half_light_radius=1, gsparams=gsparams) gal4 = galsim.Sersic(half_light_radius=1, n=2.5, gsparams=gsparams) # A Sersic profile may be truncated if desired. # The units for this are expected to be arcsec (or specifically -- whatever units # you are using for all the size values as defined by the pixel_scale). bulge = galsim.Sersic(half_light_radius=0.7, n=3.2, trunc=8.5, gsparams=gsparams) disk = galsim.Sersic(half_light_radius=1.2, n=1.5, gsparams=gsparams) gal5 = 0.4 * bulge + 0.6 * disk # Net half-light radius is only approximate for this one. gals = [gal1, gal2, gal3, gal4, gal5] gal_names = [ "Gaussian", "Exponential", "Devaucouleurs", "n=2.5 Sersic", "Bulge + Disk" ] gal_times = [0, 0, 0, 0, 0] gal_fft_times = [0, 0, 0, 0, 0] gal_phot_times = [0, 0, 0, 0, 0] # Other times to keep track of: setup_times = 0 fft_times = 0 phot_times = 0 noise_times = 0 # Loop over combinations of psf, gal, and make 4 random choices for flux, size, shape. all_images = [] k = 0 for ipsf in range(len(psfs)): psf = psfs[ipsf] psf_name = psf_names[ipsf] logger.info('psf %d: %s', ipsf + 1, psf) # Note that this implicitly calls str(psf). We've made an effort to give all GalSim # objects an informative but relatively succinct str representation. Some details may # be missing, but it should look essentially like how you would create the object. logger.debug('repr = %r', psf) # The repr() version are a bit more pedantic in form and should be completely informative, # to the point where two objects that are not identical should never have equal repr # strings. As such the repr strings may in some cases be somewhat unwieldy. For instance, # since we set non-default gsparams in these, the repr includes that information, but # it is omitted from the str for brevity. for igal in range(len(gals)): gal = gals[igal] gal_name = gal_names[igal] logger.info(' galaxy %d: %s', igal + 1, gal) logger.debug(' repr = %r', gal) for i in range(4): logger.debug(' Start work on image %d', i) t1 = time.time() # Initialize the random number generator we will be using. rng = galsim.UniformDeviate(random_seed + k) # Generate random variates: flux = rng() * (gal_flux_max - gal_flux_min) + gal_flux_min # Use a new variable name, since we'll want to keep the original unmodified. this_gal = gal.withFlux(flux) hlr = rng() * (gal_hlr_max - gal_hlr_min) + gal_hlr_min this_gal = this_gal.dilate(hlr) beta_ellip = rng() * 2 * math.pi * galsim.radians ellip = rng() * (gal_e_max - gal_e_min) + gal_e_min gal_shape = galsim.Shear(e=ellip, beta=beta_ellip) this_gal = this_gal.shear(gal_shape) # Build the final object by convolving the galaxy and PSF. final = galsim.Convolve([this_gal, psf]) # Create the large, double width output image # Rather than provide a scale= argument to the drawImage commands, we can also # set the pixel scale in the image constructor. # Note: You can also change it after the construction with im.scale=pixel_scale image = galsim.ImageF(2 * nx + 2, ny, scale=pixel_scale) # Assign the following two Image "views", fft_image and phot_image. # Using the syntax below, these are views into the larger image. # Changes/additions to the sub-images referenced by the views are automatically # reflected in the original image. fft_image = image[galsim.BoundsI(1, nx, 1, ny)] phot_image = image[galsim.BoundsI(nx + 3, 2 * nx + 2, 1, ny)] logger.debug( ' Read in training sample galaxy and PSF from file') t2 = time.time() # Draw the profile # This default rendering method (method='auto') usually defaults to FFT, since # that is normally the most efficient method. However, we can also set method # to 'fft' explicitly to force it to always use FFTs for the convolution # by the pixel response. (In this case, it doesn't have any effect, since # the 'auto' method would have always chosen 'fft' anyway, so this is just # for illustrative purposes.) final.drawImage(fft_image, method='fft') logger.debug( ' Drew fft image. Total drawn flux = %f. .flux = %f', fft_image.array.sum(), final.getFlux()) t3 = time.time() # Add Poisson noise sky_level_pixel = sky_level * pixel_scale**2 fft_image.addNoise( galsim.PoissonNoise(rng, sky_level=sky_level_pixel)) t4 = time.time() # The next two lines are just to get the output from this demo script # to match the output from the parsing of demo7.yaml. rng = galsim.UniformDeviate(random_seed + k) rng() rng() rng() rng() # Repeat for photon shooting image. # The max_extra_noise parameter indicates how much extra noise per pixel we are # willing to tolerate. The sky noise will be adding a variance of sky_level_pixel, # so we allow up to 1% of that extra. final.drawImage(phot_image, method='phot', max_extra_noise=sky_level_pixel / 100, rng=rng) t5 = time.time() # For photon shooting, galaxy already has Poisson noise, so we want to make # sure not to add that noise again! Thus, we just add sky noise, which # is Poisson with the mean = sky_level_pixel pd = galsim.PoissonDeviate(rng, mean=sky_level_pixel) # DeviateNoise just adds the action of the given deviate to every pixel. phot_image.addNoise(galsim.DeviateNoise(pd)) # For PoissonDeviate, the mean is not zero, so for a background-subtracted # image, we need to subtract the mean back off when we are done. phot_image -= sky_level_pixel logger.debug( ' Added Poisson noise. Image fluxes are now %f and %f', fft_image.array.sum(), phot_image.array.sum()) t6 = time.time() # Store that into the list of all images all_images += [image] k = k + 1 logger.info( ' %d: flux = %.2e, hlr = %.2f, ellip = (%.2f,%.2f)', k, flux, hlr, gal_shape.getE1(), gal_shape.getE2()) logger.debug(' Times: %f, %f, %f, %f, %f', t2 - t1, t3 - t2, t4 - t3, t5 - t4, t6 - t5) psf_times[ipsf] += t6 - t1 psf_fft_times[ipsf] += t3 - t2 psf_phot_times[ipsf] += t5 - t4 gal_times[igal] += t6 - t1 gal_fft_times[igal] += t3 - t2 gal_phot_times[igal] += t5 - t4 setup_times += t2 - t1 fft_times += t3 - t2 phot_times += t5 - t4 noise_times += t4 - t3 + t6 - t5 logger.info('Done making images of galaxies') logger.info('') logger.info('Some timing statistics:') logger.info(' Total time for setup steps = %f', setup_times) logger.info(' Total time for regular fft drawing = %f', fft_times) logger.info(' Total time for photon shooting = %f', phot_times) logger.info(' Total time for adding noise = %f', noise_times) logger.info('') logger.info('Breakdown by PSF type:') for ipsf in range(len(psfs)): logger.info(' %s: Total time = %f (fft: %f, phot: %f)', psf_names[ipsf], psf_times[ipsf], psf_fft_times[ipsf], psf_phot_times[ipsf]) logger.info('') logger.info('Breakdown by Galaxy type:') for igal in range(len(gals)): logger.info(' %s: Total time = %f (fft: %f, phot: %f)', gal_names[igal], gal_times[igal], gal_fft_times[igal], gal_phot_times[igal]) logger.info('') # Now write the image to disk. # With any write command, you can optionally compress the file using several compression # schemes: # 'gzip' uses gzip on the full output file. # 'bzip2' uses bzip2 on the full output file. # 'rice' uses rice compression on the image, leaving the fits headers readable. # 'gzip_tile' uses gzip in tiles on the output image, leaving the fits headers readable. # 'hcompress' uses hcompress on the image, but it is only valid for 2-d data, so it # doesn't work for writeCube. # 'plio' uses plio on the image, but it is only valid for positive integer data. # Furthermore, the first three have standard filename extensions associated with them, # so if you don't specify a compression, but the filename ends with '.gz', '.bz2' or '.fz', # the corresponding compression will be selected automatically. # In other words, the `compression='gzip'` specification is actually optional here: galsim.fits.writeCube(all_images, file_name, compression='gzip') logger.info('Wrote fft image to fits data cube %r', file_name)
def getPSF(SCAs=None, approximate_struts=False, n_waves=None, extra_aberrations=None, wavelength_limits=None, logger=None, wavelength=None, high_accuracy=False, gsparams=None): """ Get the PSF for WFIRST observations. By default, this routine returns a dict of ChromaticOpticalPSF objects, with the dict indexed by the SCA (Sensor Chip Array, the equivalent of a chip in an optical CCD). The PSF for a given SCA corresponds to that for the center of the SCA. Currently we do not use information about PSF variation within each SCA, which is relatively small. This routine also takes an optional keyword `SCAs`, which can be a single number or an iterable; if this is specified then results are not included for the other SCAs. The default is to do the calculations using the full specification of the WFIRST pupil plane, which is a costly calculation in terms of memory. For this, we use the provided pupil plane for red bands from http://wfirst.gsfc.nasa.gov/science/sdt_public/wps/references/instrument/ (Cycle 5) and we neglect for now the fact that the pupil plane configuration is slightly different for imaging in Z087, Y106, J129. To avoid using the full pupil plane configuration, use the optional keyword `approximate_struts`. In this case, the pupil plane will have the correct obscuration and number of struts, but the struts will be purely radial and evenly spaced instead of the true configuration. The simplicity of this arrangement leads to a much faster calculation, and somewhat simplifies the configuration of the diffraction spikes. Also note that currently the orientation of the struts is fixed, rather than rotating depending on the orientation of the focal plane. Rotation of the PSF can easily be affected by the user via psf = galsim.wfirst.getPSF(...).rotate(angle) which will rotate the entire PSF (including the diffraction spikes and any other features). The calculation takes advantage of the fact that the diffraction limit and aberrations have a simple, understood wavelength-dependence. (The WFIRST project webpage for Cycle 5 does in fact provide aberrations as a function of wavelength, but the deviation from the expected chromatic dependence is very small and we neglect it here.) For reference, the script use to parse the Zernikes given on the webpage and create the files in the GalSim repository can be found in `devel/external/parse_wfirst_zernikes_0715.py`. The resulting chromatic object can be used to draw into any of the WFIRST bandpasses. For applications that require very high accuracy in the modeling of the PSF, with very limited aliasing, the `high_accuracy` option can be set to True. When using this option, the MTF has a value below 1e-4 for all wavenumbers above the band limit when using `approximate_struts=True`, or below 3e-4 when using `approximate_struts=False`. In contrast, when `high_accuracy=False` (the default), there are some bumps in the MTF above the band limit that reach an amplitude of ~1e-2. By default, no additional aberrations are included above the basic design. However, users can provide an optional keyword `extra_aberrations` that will be included on top of those that are part of the design. This should be in the same format as for the ChromaticOpticalPSF class, with units of waves at the fiducial wavelength, 1293 nm. Currently, only aberrations up to order 11 (Noll convention) can be simulated. For WFIRST, the current tolerance for additional aberrations is a total of 90 nanometers RMS: http://wfirst.gsfc.nasa.gov/science/sdt_public/wps/references/instrument/README_AFTA_C5_WFC_Zernike_and_Field_Data.pdf distributed largely among coma, astigmatism, trefoil, and spherical aberrations (NOT defocus). This information might serve as a guide for reasonable `extra_aberrations` inputs. Jitter and charge diffusion are, by default, not included. Users who wish to include these can find some guidelines for typical length scales of the Gaussians that can represent these effects, and convolve the ChromaticOpticalPSF with appropriate achromatic Gaussians. The PSFs are always defined assuming the user will specify length scales in arcsec. @param SCAs Specific SCAs for which the PSF should be loaded. This can be either a single number or an iterable. If None, then the PSF will be loaded for all SCAs (1...18). Note that the object that is returned is a dict indexed by the requested SCA indices. [default: None] @param approximate_struts Should the routine use an approximate representation of the pupil plane, with 6 equally-spaced radial struts, instead of the exact representation of the pupil plane? Setting this parameter to True will lead to faster calculations, with a slightly less realistic PSFs. [default: False] @param n_waves Number of wavelengths to use for setting up interpolation of the chromatic PSF objects, which can lead to much faster image rendering. If None, then no interpolation is used. Note that users who want to interpolate can always set up the interpolation later on even if they do not do so when calling getPSF(). [default: None] @param extra_aberrations Array of extra aberrations to include in the PSF model, on top of those that are part of the WFIRST design. These should be provided in units of waves at the fiducial wavelength of 1293 nm, as an array of length 12 with entries 4 through 11 corresponding to defocus through spherical aberrations. [default: None] @param wavelength_limits A tuple or list of the blue and red wavelength limits to use for interpolating the chromatic object, if `n_waves` is not None. If None, then it uses the blue and red limits of all imaging passbands to determine the most inclusive wavelength range possible. But this keyword can be used to reduce the range of wavelengths if only one passband (or a subset of passbands) is to be used for making the images. [default: None] @param logger A logger object for output of progress statements if the user wants them. [default: None] @param wavelength An option to get an achromatic PSF for a single wavelength, for users who do not care about chromaticity of the PSF. If None, then the fully chromatic PSF is returned. Alternatively the user should supply either (a) a wavelength in nanometers, and they will get achromatic OpticalPSF objects for that wavelength, or (b) a bandpass object, in which case they will get achromatic OpticalPSF objects defined at the effective wavelength of that bandpass. [default: False] @param high_accuracy If True, make higher-fidelity representations of the PSF in Fourier space, to minimize aliasing (see plots on https://github.com/GalSim-developers/GalSim/issues/661 for more details). This setting is more expensive in terms of time and RAM, and may not be necessary for many applications. [default: False] @param gsparams An optional GSParams argument. See the docstring for GSParams for details. [default: None] @returns A dict of ChromaticOpticalPSF or OpticalPSF objects for each SCA. """ # Check which SCAs are to be done using a helper routine in this module. SCAs = galsim.wfirst._parse_SCAs(SCAs) # Deal with some accuracy settings. if high_accuracy: if approximate_struts: oversampling = 3.5 else: oversampling = 2.0 # In this case, we need to pad the edges of the pupil plane image, so we cannot just use # the stored file. tmp_pupil_plane_im = galsim.fits.read( galsim.wfirst.pupil_plane_file) old_bounds = tmp_pupil_plane_im.bounds new_bounds = old_bounds.withBorder( (old_bounds.xmax + 1 - old_bounds.xmin) / 2) pupil_plane_im = galsim.Image(bounds=new_bounds) pupil_plane_im[old_bounds] = tmp_pupil_plane_im pupil_plane_scale = galsim.wfirst.pupil_plane_scale else: if approximate_struts: oversampling = 1.5 else: oversampling = 1.2 pupil_plane_im = galsim.wfirst.pupil_plane_file pupil_plane_scale = galsim.wfirst.pupil_plane_scale if wavelength is None: if n_waves is not None: if wavelength_limits is None: # To decide the range of wavelengths to use (if none were passed in by the user), # first check out all the bandpasses. bandpass_dict = galsim.wfirst.getBandpasses() # Then find the blue and red limit to be used for the imaging bandpasses overall. blue_limit, red_limit = _find_limits(default_bandpass_list, bandpass_dict) else: if not isinstance(wavelength_limits, tuple): raise ValueError( "Wavelength limits must be entered as a tuple!") blue_limit, red_limit = wavelength_limits if red_limit <= blue_limit: raise ValueError( "Wavelength limits must have red_limit > blue_limit." "Input: blue limit=%f, red limit=%f nanometers" % (blue_limit, red_limit)) else: if isinstance(wavelength, galsim.Bandpass): wavelength_nm = wavelength.effective_wavelength elif isinstance(wavelength, float): wavelength_nm = wavelength else: raise TypeError( "Keyword 'wavelength' should either be a Bandpass, float," " or None.") # Start reading in the aberrations for the relevant SCAs. aberration_dict = {} PSF_dict = {} if logger: logger.debug('Beginning to loop over SCAs and get the PSF:') for SCA in SCAs: aberration_dict[SCA] = _read_aberrations(SCA) use_aberrations = aberration_dict[SCA] if extra_aberrations is not None: use_aberrations += extra_aberrations # We don't want to use piston, tip, or tilt aberrations. The former doesn't affect the # appearance of the PSF, and the latter cause centroid shifts. So, we set the first 4 # numbers (corresponding to a place-holder, piston, tip, and tilt) to zero. use_aberrations[0:4] = 0. # Now set up the PSF for this SCA, including the option to simplify the pupil plane. if logger: logger.debug(' ... SCA %d' % SCA) if wavelength is None: if approximate_struts: PSF = galsim.ChromaticOpticalPSF( lam=zemax_wavelength, diam=galsim.wfirst.diameter, aberrations=use_aberrations, obscuration=galsim.wfirst.obscuration, nstruts=6, oversampling=oversampling, gsparams=gsparams) else: PSF = galsim.ChromaticOpticalPSF( lam=zemax_wavelength, diam=galsim.wfirst.diameter, aberrations=use_aberrations, obscuration=galsim.wfirst.obscuration, pupil_plane_im=pupil_plane_im, pupil_plane_scale=pupil_plane_scale, oversampling=oversampling, pad_factor=2., gsparams=gsparams) if n_waves is not None: PSF = PSF.interpolate(waves=np.linspace( blue_limit, red_limit, n_waves), oversample_fac=1.5) else: tmp_aberrations = use_aberrations * zemax_wavelength / wavelength_nm if approximate_struts: PSF = galsim.OpticalPSF(lam=wavelength_nm, diam=galsim.wfirst.diameter, aberrations=tmp_aberrations, obscuration=galsim.wfirst.obscuration, nstruts=6, oversampling=oversampling, gsparams=gsparams) else: PSF = galsim.OpticalPSF(lam=wavelength_nm, diam=galsim.wfirst.diameter, aberrations=tmp_aberrations, obscuration=galsim.wfirst.obscuration, pupil_plane_im=pupil_plane_im, pupil_plane_scale=pupil_plane_scale, oversampling=oversampling, pad_factor=2., gsparams=gsparams) PSF_dict[SCA] = PSF return PSF_dict
def main(argv): logging.basicConfig(format="%(message)s", level=logging.INFO, stream=sys.stdout) logger = logging.getLogger("demo7") ### Make output directory if not already present. if not os.path.isdir('output'): os.mkdir('output') file_name = os.path.join('output','cube_phot.fits.gz') scaleimg_y=[] scaleimg_x_DFT=[] scaleimg_x_Photon=[] ranges = np.linspace(10,50,5) for num in ranges: ### Define some parameters we'll use below. random_seed = 553728 sky_level = 1.e4 # ADU / arcsec^2 pixel_scale =.28 # arcsec* nx = num ny = num gal_flux_min = 1.e1 # Range for galaxy flux gal_flux_max = 1.e5 gal_hlr_min = 0.3 # arcsec gal_hlr_max = 1.3 # arcsec gal_e_min = 0. # Range for ellipticity gal_e_max = 0.8 psf_fwhm = 0.65 # arcsec # We encapsulate these parameters with an object called GSParams. The default values # are intended to be accurate enough for normal precision shear tests, without sacrificing # too much speed.Any PSF or galaxy object can be given a gsparams argument on construction that can # have different values to make the calculation more or less accurate (typically trading # off for speed or memory). gsparams = galsim.GSParams( folding_threshold=1.e-2, # maximum fractional flux that may be folded around edge of FFT maxk_threshold=2.e-3, # k-values less than this may be excluded off edge of FFT xvalue_accuracy=1.e-4, # approximations in real space aim to be this accurate kvalue_accuracy=1.e-4, # approximations in fourier space aim to be this accurate shoot_accuracy=1.e-4, # approximations in photon shooting aim to be this accurate minimum_fft_size=64) # minimum size of ffts logger.info('Starting psf') # Make the PSF profiles: ### psf 1 psf1 = galsim.Gaussian(fwhm = psf_fwhm, gsparams=gsparams) ### psf 2 psf2 = galsim.Moffat(fwhm = psf_fwhm, beta = 2.4, gsparams=gsparams) ### psf 3 psf3_inner = galsim.Gaussian(fwhm = psf_fwhm, flux = 0.8, gsparams=gsparams) psf3_outer = galsim.Gaussian(fwhm = 2*psf_fwhm, flux = 0.2, gsparams=gsparams) psf3 = psf3_inner + psf3_outer atmos = galsim.Gaussian(fwhm = psf_fwhm, gsparams=gsparams) ### defining the telescope # The OpticalPSF and set of Zernike values chosen below correspond to a reasonably well aligned, # smallish ~0.3m / 12 inch diameter telescope with a central obscuration of ~0.12m or 5 inches # diameter, being used in optical wavebands. aberrations = [ 0.0 ] * 12 # Set the initial size. aberrations[4] = 0.06 # Noll index 4 = Defocus aberrations[5:7] = [ 0.12, -0.08 ] # Noll index 5,6 = Astigmatism aberrations[7:9] = [ 0.07, 0.04 ] # Noll index 7,8 = Coma aberrations[11] = -0.13 # Noll index 11 = Spherical optics = galsim.OpticalPSF( lam_over_diam = 0.6 * psf_fwhm, obscuration = 0.4, aberrations = aberrations, gsparams=gsparams) ### psf 4 psf4 = galsim.Convolve([atmos, optics]) # Convolve inherits the gsparams from the first # item in the list. (Or you can supply a gsparams # argument explicitly if you want to override this.) atmos = galsim.Kolmogorov(fwhm = psf_fwhm, gsparams=gsparams) optics = galsim.Airy(lam_over_diam = 0.3 * psf_fwhm, gsparams=gsparams) ### psf 5 psf5 = galsim.Convolve([atmos,optics]) ### define where to keep the psfs info psfs = [psf1, psf2, psf3, psf4, psf5] psf_names = ["Gaussian", "Moffat", "Double Gaussian", "OpticalPSF", "Kolmogorov * Airy"] psf_times = [0,0,0,0,0] psf_fft_times = [0,0,0,0,0] psf_phot_times = [0,0,0,0,0] # Make the galaxy profiles: ### gal 1 gal1 = galsim.Gaussian(half_light_radius = 1, gsparams=gsparams) ### gal 2 gal2 = galsim.Exponential(half_light_radius = 1, gsparams=gsparams) ### gal 3 gal3 = galsim.DeVaucouleurs(half_light_radius = 1, gsparams=gsparams) ### gal 4 gal4 = galsim.Sersic(half_light_radius = 1, n = 2.5, gsparams=gsparams) bulge = galsim.Sersic(half_light_radius = 0.7, n = 3.2, trunc = 8.5, gsparams=gsparams) disk = galsim.Sersic(half_light_radius = 1.2, n = 1.5, gsparams=gsparams) ### gal 5 gal5 = 0.4*bulge + 0.6*disk # Net half-light radius is only approximate for this one. ### define where to keep the galaxys info gals = [gal1, gal2, gal3, gal4, gal5] gal_names = ["Gaussian", "Exponential", "Devaucouleurs", "n=2.5 Sersic", "Bulge + Disk"] gal_times = [0,0,0,0,0] gal_fft_times = [0,0,0,0,0] gal_phot_times = [0,0,0,0,0] ### initial time conditions # Other times to keep track of: setup_times = 0 fft_times = 0 phot_times = 0 noise_times = 0 # Loop over combinations of psf, gal, and make 4 random choices for flux, size, shape. all_images = [] k = 0 n=[] x_DFT=[] x_Photon=[] y=[] all_fluxes = np.linspace(gal_flux_min,gal_flux_max,100) ### will loop through the numbers (amount) of psfs for ipsf in range(len(psfs)): ### calls on the 5 psfs and their names psf = psfs[ipsf] psf_name = psf_names[ipsf] ### outputs the psf number and the psf information needed to create an object logger.info('psf %d: %s',ipsf+1,psf) logger.debug('repr = %r',psf) ### will loop through the numbers (amount) of galaxies for igal in range(len(gals)): ### calls on the 5 galaxies and their names gal = gals[igal] gal_name = gal_names[igal] ### outputs the psf number and the psf information needed to create an object logger.info(' galaxy %d: %s',igal+1,gal) logger.debug(' repr = %r',gal) ### will loop though 0,1,2,3 flux, size, and shape to create 4 images for each ### combination of galaxy and psf for i in range(4): logger.debug(' Start work on image %d',i) all_fluxes_i = all_fluxes[i] image, t1, t2, t3, t4, t5, t6, k, flux, hlr, gal_shape, y_i, psfs, gals, file_name = func(file_name, random_seed, pixel_scale, nx, ny, sky_level, gal_flux_min, gal_flux_max, gal_hlr_min, gal_hlr_max, gal_e_min, gal_e_max, psf_fwhm, gsparams, psf1, psf2, psf3_inner, psf3_outer, psf3, atmos, aberrations, psf4, optics, psf5, psfs, psf_names, psf_times, psf_fft_times, psf_phot_times, gal1, gal2, gal3, gal4, bulge, disk, gal5, gals, gal_names, gal_times, gal_fft_times, gal_phot_times, setup_times, fft_times, phot_times, noise_times, k, all_fluxes_i, psf, psf_name, gal, gal_name) y = np.append(y, y_i) ### Store that into the list of all images all_images += [image] ### add an itteration though the loop for the psf and galaxy combination images k = k+1 ### express the flux,hlr, and ellip of each image combination,4 for every loop logger.info(' %d: flux = %.2e, hlr = %.2f, ellip = (%.2f,%.2f)', k, flux, hlr, gal_shape.getE1(), gal_shape.getE2()) logger.debug(' Times: %f, %f, %f, %f, %f',t2-t1, t3-t2, t4-t3, t5-t4, t6-t5) psf_times[ipsf] += t6-t1 psf_fft_times[ipsf] += t3-t2 psf_phot_times[ipsf] += t5-t4 gal_times[igal] += t6-t1 gal_fft_times[igal] += t3-t2 gal_phot_times[igal] += t5-t4 setup_times += t2-t1 fft_times += t3-t2 phot_times += t5-t4 noise_times += t4-t3 + t6-t5 x_DFT = np.append(x_DFT,gal_fft_times[igal]) x_Photon = np.append(x_Photon,gal_phot_times[igal]) #### flux and time of each galaxy profile with each PSF scaleimg_y=np.append(scaleimg_y,y) scaleimg_x_DFT=np.append(scaleimg_x_DFT,x_DFT) scaleimg_x_Photon=np.append(scaleimg_x_Photon,x_Photon) print 'noise DFT =', n #### [FOR FIGURES 1-5] the DFT and Photon-Shooting time for the first psf and 5 galaxy profile all at flux 10 x1_DFT_1=(scaleimg_x_DFT[0],scaleimg_x_DFT[100],scaleimg_x_DFT[200],scaleimg_x_DFT[300],scaleimg_x_DFT[400]) x1_DFT_2=(scaleimg_x_DFT[4],scaleimg_x_DFT[104],scaleimg_x_DFT[204],scaleimg_x_DFT[304],scaleimg_x_DFT[404]) x1_DFT_3=(scaleimg_x_DFT[8],scaleimg_x_DFT[108],scaleimg_x_DFT[208],scaleimg_x_DFT[308],scaleimg_x_DFT[408]) x1_DFT_4=(scaleimg_x_DFT[12],scaleimg_x_DFT[112],scaleimg_x_DFT[212],scaleimg_x_DFT[312],scaleimg_x_DFT[412]) x1_DFT_5=(scaleimg_x_DFT[16],scaleimg_x_DFT[116],scaleimg_x_DFT[216],scaleimg_x_DFT[316],scaleimg_x_DFT[416]) x1_Photon_1=(scaleimg_x_Photon[0],scaleimg_x_Photon[100],scaleimg_x_Photon[200],scaleimg_x_Photon[300],scaleimg_x_Photon[400]) x1_Photon_2=(scaleimg_x_Photon[4],scaleimg_x_Photon[104],scaleimg_x_Photon[204],scaleimg_x_Photon[304],scaleimg_x_Photon[404]) x1_Photon_3=(scaleimg_x_Photon[8],scaleimg_x_Photon[108],scaleimg_x_Photon[208],scaleimg_x_Photon[308],scaleimg_x_Photon[408]) x1_Photon_4=(scaleimg_x_Photon[12],scaleimg_x_Photon[112],scaleimg_x_Photon[212],scaleimg_x_Photon[312],scaleimg_x_Photon[412]) x1_Photon_5=(scaleimg_x_Photon[16],scaleimg_x_Photon[116],scaleimg_x_Photon[216],scaleimg_x_Photon[316],scaleimg_x_Photon[416]) #### [FOR FIGURES 6-10] for the first PSF and 5 galaxy profile for all flux at 1020 x2_DFT_1=(scaleimg_x_DFT[1],scaleimg_x_DFT[101],scaleimg_x_DFT[201],scaleimg_x_DFT[301],scaleimg_x_DFT[401]) x2_DFT_2=(scaleimg_x_DFT[2],scaleimg_x_DFT[102],scaleimg_x_DFT[202],scaleimg_x_DFT[302],scaleimg_x_DFT[402]) x2_DFT_3=(scaleimg_x_DFT[3],scaleimg_x_DFT[103],scaleimg_x_DFT[203],scaleimg_x_DFT[303],scaleimg_x_DFT[403]) x2_DFT_4=(scaleimg_x_DFT[4],scaleimg_x_DFT[104],scaleimg_x_DFT[204],scaleimg_x_DFT[304],scaleimg_x_DFT[404]) x2_DFT_5=(scaleimg_x_DFT[5],scaleimg_x_DFT[105],scaleimg_x_DFT[205],scaleimg_x_DFT[305],scaleimg_x_DFT[405]) x2_Photon_1=(scaleimg_x_Photon[1],scaleimg_x_Photon[101],scaleimg_x_Photon[201],scaleimg_x_Photon[301],scaleimg_x_Photon[401]) x2_Photon_2=(scaleimg_x_Photon[2],scaleimg_x_Photon[102],scaleimg_x_Photon[202],scaleimg_x_Photon[302],scaleimg_x_Photon[402]) x2_Photon_3=(scaleimg_x_Photon[3],scaleimg_x_Photon[103],scaleimg_x_Photon[203],scaleimg_x_Photon[303],scaleimg_x_Photon[403]) x2_Photon_4=(scaleimg_x_Photon[4],scaleimg_x_Photon[104],scaleimg_x_Photon[204],scaleimg_x_Photon[304],scaleimg_x_Photon[404]) x2_Photon_5=(scaleimg_x_Photon[5],scaleimg_x_Photon[105],scaleimg_x_Photon[205],scaleimg_x_Photon[305],scaleimg_x_Photon[405]) logger.info('Done making images of galaxies') ##### [FOR FIGURES 1-5] the DFT and Photon-Shooting time for the first psf and 5 galaxy profile all at flux 10 ###subtraction of points xnew_1=[] xnew_2=[] xnew_3=[] xnew_4=[] xnew_5=[] for a,b,c,d,e,f,g,h,i,j in zip (x1_DFT_1,x1_Photon_1,x1_DFT_2,x1_Photon_2,x1_DFT_3,x1_Photon_3,x1_DFT_4,x1_Photon_4,x1_DFT_5,x1_Photon_5): x1=a-b x2=c-d x3=e-f x4=g-h x5=i-j xnew_1= np.append(xnew_1,x1) xnew_2= np.append(xnew_2,x2) xnew_3= np.append(xnew_3,x3) xnew_4= np.append(xnew_4,x4) xnew_5= np.append(xnew_5,x5) #### [FOR FIGURES 6-10] for the first PSF and 5 galaxy profile for all flux at 1020 xnew_6=[] xnew_7=[] xnew_8=[] xnew_9=[] xnew_10=[] for a,b,c,d,e,f,g,h,i,j in zip (x2_DFT_1,x2_Photon_1,x2_DFT_2,x2_Photon_2,x2_DFT_3,x2_Photon_3,x2_DFT_4,x2_Photon_4,x2_DFT_5,x2_Photon_5): x1=a-b x2=c-d x3=e-f x4=g-h x5=i-j xnew_6= np.append(xnew_6,x1) xnew_7= np.append(xnew_7,x2) xnew_8= np.append(xnew_8,x3) xnew_9= np.append(xnew_9,x4) xnew_10= np.append(xnew_10,x5) y = [0,0,0,0,0] #fig1 f, (ax1, ax2) = plt.subplots(2, sharex=True, sharey=True) l1, = ax1.plot(ranges,x1_DFT_1, 'b-', label='l1') l2, = ax1.plot(ranges,x1_Photon_1, 'g-', label='l2') l3, = ax1.plot(ranges,y,'r--',label='l3') plt.legend([l1,l2,l3],['DFT','Photon','Y=0'], loc='upper right') ax1.set_title('Gaussian PSF with Gaussian Galaxy Profile at flux=10') l4, = ax2.plot(ranges,xnew_1,'y-', label='l4') l5, = ax2.plot(ranges,y,'r--', label='l5') plt.legend([l4,l5],['DFT - Photon','Y=0'], loc='upper right') plt.xlabel('pixel scale') plt.ylabel('time') f.subplots_adjust(hspace=0) plt.setp([a.get_xticklabels() for a in f.axes[:-1]], visible=False) plt.savefig('n_1.png') #fig2 f, (ax1, ax2) = plt.subplots(2, sharex=True, sharey=True) l1, = ax1.plot(ranges,x1_DFT_2, 'b-', label='l1') l2, = ax1.plot(ranges,x1_Photon_2, 'g-', label='l2') l3, = ax1.plot(ranges,y,'r--', label='l3') plt.legend([l1,l2,l3],['DFT','Photon','Y=0'], loc='upper right') ax1.set_title('Gaussian PSF with Exponential Galaxy Profile at flux=10') l4, = ax2.plot(ranges,xnew_2,'y-', label='l4') l5, = ax2.plot(ranges,y,'r--', label='l5') plt.legend([l4,l5],['DFT - Photon','Y=0'], loc='upper right') plt.xlabel('pixel scale') plt.ylabel('time') plt.legend(loc='best') f.subplots_adjust(hspace=0) plt.setp([a.get_xticklabels() for a in f.axes[:-1]], visible=False) plt.savefig('n_2.png') #fig3 f, (ax1, ax2) = plt.subplots(2, sharex=True, sharey=True) l1, = ax1.plot(ranges,x1_DFT_3, 'b-', label='l1') l2, = ax1.plot(ranges,x1_Photon_3, 'g-', label='l2') l3, = ax1.plot(ranges,y,'r--', label='l3') plt.legend([l1,l2,l3],['DFT','Photon','Y=0'], loc='upper right') ax1.set_title('Gaussian PSF with Devaucouleurs Galaxy Profile at flux=10') l4, = ax2.plot(ranges,xnew_3, 'y-', label='l4') l5, = ax2.plot(ranges,y,'r--', label='l5') plt.legend([l4,l5],['DFT - Photon','Y=0'], loc='upper right') plt.xlabel('pixel scale') plt.ylabel('time') plt.legend(loc='best') f.subplots_adjust(hspace=0) plt.setp([a.get_xticklabels() for a in f.axes[:-1]], visible=False) plt.savefig('n_3.png') #fig4 f, (ax1, ax2) = plt.subplots(2, sharex=True, sharey=True) l1, = ax1.plot(ranges,x1_DFT_4, 'b-', label='l1') l2, = ax1.plot(ranges,x1_Photon_4, 'g-', label='l2') l3, = ax1.plot(ranges,y,'r--', label='l3') plt.legend([l1,l2,l3],['DFT','Photon','Y=0'], loc='upper right') ax1.set_title('Gaussian PSF with n=2.5 Sersic Galaxy Profile at flux=10') l4, = ax2.plot(ranges,xnew_4, 'y-', label='l4') l5, = ax2.plot(ranges,y,'r--', label='l5') plt.legend([l4,l5],['DFT - Photon','Y=0'], loc='upper right') plt.xlabel('pixel scale') plt.ylabel('time') plt.legend(loc='best') f.subplots_adjust(hspace=0) plt.setp([a.get_xticklabels() for a in f.axes[:-1]], visible=False) plt.savefig('n_4.png') #fig5 f, (ax1, ax2) = plt.subplots(2, sharex=True, sharey=True) l1, = ax1.plot(ranges,x1_DFT_5, 'b-', label='l1') l2, = ax1.plot(ranges,x1_Photon_5, 'g-', label='l2') l3, = ax1.plot(ranges,y,'r--', label='l3') plt.legend([l1,l2,l3],['DFT','Photon','Y=0'], loc='upper right') ax1.set_title('Gaussian PSF with Bulge + Disk Galaxy Profile at flux=10') l4, = ax2.plot(ranges,xnew_5, 'y-', label='l4') l5, = ax2.plot(ranges,y,'r--', label='l5') plt.legend([l4,l5],['DFT - Photon','Y=0'], loc='upper right') plt.xlabel('pixel scale') plt.ylabel('time') plt.legend(loc='best') f.subplots_adjust(hspace=0) plt.setp([a.get_xticklabels() for a in f.axes[:-1]], visible=False) plt.savefig('n_5.png') #fig6 f, (ax1, ax2) = plt.subplots(2, sharex=True, sharey=True) l1, = ax1.plot(ranges,x2_DFT_1, 'b-', label='l1') l2, = ax1.plot(ranges,x2_Photon_1, 'g-', label='l2') l3, = ax1.plot(ranges,y,'r--', label='l3') plt.legend([l1,l2,l3],['DFT','Photon','Y=0'], loc='upper right') ax1.set_title('Gaussian PSF with Bulge + Disk Galaxy Profile at flux=1020') l4, = ax2.plot(ranges,xnew_6, 'y-', label='l4') l5, = ax2.plot(ranges,y,'r--', label='l5') plt.legend([l4,l5],['DFT - Photon','Y=0'], loc='upper right') plt.xlabel('pixel scale') plt.ylabel('time') plt.legend(loc='best') f.subplots_adjust(hspace=0) plt.setp([a.get_xticklabels() for a in f.axes[:-1]], visible=False) plt.savefig('n_6.png') #fig7 f, (ax1, ax2) = plt.subplots(2, sharex=True, sharey=True) l1, = ax1.plot(ranges,x2_DFT_2, 'b-', label='l1') l2, = ax1.plot(ranges,x2_Photon_2, 'g-', label='l2') l3, = ax1.plot(ranges,y,'r--', label='l3') plt.legend([l1,l2,l3],['DFT','Photon','Y=0'], loc='upper right') ax1.set_title('Gaussian PSF with Bulge + Disk Galaxy Profile at flux=1020') l4, = ax2.plot(ranges,xnew_7, 'y-', label='l4') l5, = ax2.plot(ranges,y,'r--', label='l5') plt.legend([l4,l5],['DFT - Photon','Y=0'], loc='upper right') plt.xlabel('pixel scale') plt.ylabel('time') plt.legend(loc='best') f.subplots_adjust(hspace=0) plt.setp([a.get_xticklabels() for a in f.axes[:-1]], visible=False) plt.savefig('n_7.png') #fig8 f, (ax1, ax2) = plt.subplots(2, sharex=True, sharey=True) l1, = ax1.plot(ranges,x2_DFT_3, 'b-', label='l1') l2, = ax1.plot(ranges,x2_Photon_3, 'g-', label='l2') l3, = ax1.plot(ranges,y,'r--', label='l3') plt.legend([l1,l2,l3],['DFT','Photon','Y=0'], loc='upper right') ax1.set_title('Gaussian PSF with Bulge + Disk Galaxy Profile at flux=1020') l4, = ax2.plot(ranges,xnew_8, 'y-', label='l4') l5, = ax2.plot(ranges,y,'r--', label='l5') plt.legend([l4,l5],['DFT - Photon','Y=0'], loc='upper right') plt.xlabel('pixel scale') plt.ylabel('time') plt.legend(loc='best') f.subplots_adjust(hspace=0) plt.setp([a.get_xticklabels() for a in f.axes[:-1]], visible=False) plt.savefig('n_8.png') #fig9 f, (ax1, ax2) = plt.subplots(2, sharex=True, sharey=True) l1, = ax1.plot(ranges,x2_DFT_4, 'b-', label='l1') l2, = ax1.plot(ranges,x2_Photon_4, 'g-', label='l2') l3, = ax1.plot(ranges,y,'r--', label='l3') plt.legend([l1,l2,l3],['DFT','Photon','Y=0'], loc='upper right') ax1.set_title('Gaussian PSF with Bulge + Disk Galaxy Profile at flux=1020') l4, = ax2.plot(ranges,xnew_9, 'y-', label='l4') l5, = ax2.plot(ranges,y,'r--', label='l5') plt.legend([l4,l5],['DFT - Photon','Y=0'], loc='upper right') plt.xlabel('pixel scale') plt.ylabel('time') plt.legend(loc='best') f.subplots_adjust(hspace=0) plt.setp([a.get_xticklabels() for a in f.axes[:-1]], visible=False) plt.savefig('n_9.png') #fig10 f, (ax1, ax2) = plt.subplots(2, sharex=True, sharey=True) l1, = ax1.plot(ranges,x2_DFT_5, 'b-', label='l1') l2, = ax1.plot(ranges,x2_Photon_5, 'g-', label='l2') l3, = ax1.plot(ranges,y,'r--', label='l3') plt.legend([l1,l2,l3],['DFT','Photon','Y=0'], loc='upper right') ax1.set_title('Gaussian PSF with Bulge + Disk Galaxy Profile at flux=1020') l4, = ax2.plot(ranges,xnew_10, 'y-', label='l4') l5, = ax2.plot(ranges,y,'r--', label='l5') plt.legend([l4,l5],['DFT - Photon','Y=0'], loc='upper right') plt.xlabel('pixel scale') plt.ylabel('time') plt.legend(loc='best') f.subplots_adjust(hspace=0) plt.setp([a.get_xticklabels() for a in f.axes[:-1]], visible=False) plt.savefig('n_10.png') plt.show() ### breakdown of psf and galaxy types as well as overal timing statistics logger.info('') logger.info('Some timing statistics:') logger.info(' Total time for setup steps = %f',setup_times) logger.info(' Total time for regular fft drawing = %f',fft_times) logger.info(' Total time for photon shooting = %f',phot_times) logger.info(' Total time for adding noise = %f',noise_times) logger.info('') logger.info('Breakdown by PSF type:') for ipsf in range(len(psfs)): logger.info(' %s: Total time = %f (fft: %f, phot: %f)', psf_names[ipsf],psf_times[ipsf],psf_fft_times[ipsf],psf_phot_times[ipsf]) logger.info('') logger.info('Breakdown by Galaxy type:') for igal in range(len(gals)): logger.info(' %s: Total time = %f (fft: %f, phot: %f)', gal_names[igal],gal_times[igal],gal_fft_times[igal],gal_phot_times[igal]) logger.info('') ### compress into a gzip file and save a a cube galsim.fits.writeCube(all_images, file_name, compression='gzip') logger.info('Wrote fft image to fits data cube %r',file_name)
def _get_single_PSF(SCA, bandpass, SCA_pos, approximate_struts, n_waves, extra_aberrations, logger, wavelength, high_accuracy, pupil_plane_type, gsparams): """Routine for making a single PSF. This gets called by getPSF() after it parses all the options that were passed in. Users will not directly interact with this routine. """ # Deal with some accuracy settings. if high_accuracy: if approximate_struts: oversampling = 3.5 else: oversampling = 2.0 # In this case, we need to pad the edges of the pupil plane image, so we cannot just use # the stored file. if pupil_plane_type == 'long': tmp_pupil_plane_im = galsim.fits.read( galsim.wfirst.pupil_plane_file_longwave) else: tmp_pupil_plane_im = galsim.fits.read( galsim.wfirst.pupil_plane_file_shortwave) old_bounds = tmp_pupil_plane_im.bounds new_bounds = old_bounds.withBorder( (old_bounds.xmax + 1 - old_bounds.xmin) / 2) pupil_plane_im = galsim.Image(bounds=new_bounds) pupil_plane_im[old_bounds] = tmp_pupil_plane_im pupil_plane_scale = galsim.wfirst.pupil_plane_scale else: if approximate_struts: oversampling = 1.5 else: oversampling = 1.2 if pupil_plane_type == 'long': pupil_plane_im = galsim.wfirst.pupil_plane_file_longwave else: pupil_plane_im = galsim.wfirst.pupil_plane_file_shortwave pupil_plane_scale = galsim.wfirst.pupil_plane_scale # Start reading in the aberrations for that SCA if logger: logger.debug('Beginning to get the PSF aberrations for SCA %d.' % SCA) aberrations, x_pos, y_pos = _read_aberrations(SCA) # Do bilinear interpolation, unless we're exactly at the center (default). use_aberrations = _interp_aberrations_bilinear(aberrations, x_pos, y_pos, SCA_pos) if extra_aberrations is not None: use_aberrations += extra_aberrations # We don't want to use piston, tip, or tilt aberrations. The former doesn't affect the # appearance of the PSF, and the latter cause centroid shifts. So, we set the first 4 # numbers (corresponding to a place-holder, piston, tip, and tilt) to zero. use_aberrations[0:4] = 0. # Now set up the PSF, including the option to simplify the pupil plane. if wavelength is None: if approximate_struts: PSF = galsim.ChromaticOpticalPSF( lam=zemax_wavelength, diam=galsim.wfirst.diameter, aberrations=use_aberrations, obscuration=galsim.wfirst.obscuration, nstruts=6, oversampling=oversampling, gsparams=gsparams) else: PSF = galsim.ChromaticOpticalPSF( lam=zemax_wavelength, diam=galsim.wfirst.diameter, aberrations=use_aberrations, obscuration=galsim.wfirst.obscuration, pupil_plane_im=pupil_plane_im, pupil_plane_scale=pupil_plane_scale, oversampling=oversampling, pad_factor=2., gsparams=gsparams) if n_waves is not None: # To decide the range of wavelengths to use, check the bandpass. bp_dict = galsim.wfirst.getBandpasses() bp = bp_dict[bandpass] PSF = PSF.interpolate(waves=np.linspace(bp.blue_limit, bp.red_limit, n_waves), oversample_fac=1.5) else: if not isinstance(wavelength, float): raise TypeError( "wavelength should either be a Bandpass, float, or None.") tmp_aberrations = use_aberrations * zemax_wavelength / wavelength if approximate_struts: PSF = galsim.OpticalPSF(lam=wavelength, diam=galsim.wfirst.diameter, aberrations=tmp_aberrations, obscuration=galsim.wfirst.obscuration, nstruts=6, oversampling=oversampling, gsparams=gsparams) else: PSF = galsim.OpticalPSF(lam=wavelength, diam=galsim.wfirst.diameter, aberrations=tmp_aberrations, obscuration=galsim.wfirst.obscuration, pupil_plane_im=pupil_plane_im, pupil_plane_scale=pupil_plane_scale, oversampling=oversampling, pad_factor=2., gsparams=gsparams) return PSF
def test_ne(): import time t1 = time.time() # Use some very forgiving settings to speed up this test. We're not actually going to draw # any images (other than internally the PSF), so should be okay. gsp1 = galsim.GSParams(maxk_threshold=5.e-2, folding_threshold=5e-2, kvalue_accuracy=1e-3, xvalue_accuracy=1e-3) gsp2 = galsim.GSParams(maxk_threshold=5.1e-2, folding_threshold=5e-2, kvalue_accuracy=1e-3, xvalue_accuracy=1e-3) pupil_plane_im = galsim.fits.read(os.path.join(imgdir, pp_file)) # Params include: lam_over_diam, (lam/diam), aberrations by name, aberrations by list, nstruts, # strut_thick, strut_angle, obscuration, oversampling, pad_factor, flux, gsparams, # circular_pupil, interpolant, pupil_plane_im, pupil_angle, scale_unit objs = [galsim.OpticalPSF(lam_over_diam=1.0, gsparams=gsp1), galsim.OpticalPSF(lam_over_diam=1.0, gsparams=gsp2), galsim.OpticalPSF(lam=1.0, diam=1.0, gsparams=gsp1), galsim.OpticalPSF(lam=1.0, diam=1.0, scale_unit=galsim.arcmin, gsparams=gsp1), galsim.OpticalPSF(lam_over_diam=1.0, defocus=0.1, gsparams=gsp1), galsim.OpticalPSF(lam_over_diam=1.0, aberrations=[0, 0, 0, 0, 0.2], gsparams=gsp1), galsim.OpticalPSF(lam_over_diam=1.0, nstruts=2, gsparams=gsp1), galsim.OpticalPSF(lam_over_diam=1.0, nstruts=2, strut_thick=0.1, gsparams=gsp1), galsim.OpticalPSF(lam_over_diam=1.0, nstruts=2, strut_angle=10.*galsim.degrees, gsparams=gsp1), galsim.OpticalPSF(lam_over_diam=1.0, obscuration=0.5, gsparams=gsp1), galsim.OpticalPSF(lam_over_diam=1.0, oversampling=2.0, gsparams=gsp1), galsim.OpticalPSF(lam_over_diam=1.0, pad_factor=2.0, gsparams=gsp1), galsim.OpticalPSF(lam_over_diam=1.0, flux=2.0, gsparams=gsp1), galsim.OpticalPSF(lam_over_diam=1.0, circular_pupil=False, gsparams=gsp1), galsim.OpticalPSF(lam_over_diam=1.0, interpolant='Linear', gsparams=gsp1)] if __name__ == "__main__": objs += [galsim.OpticalPSF(lam_over_diam=1.0, pupil_plane_im=pupil_plane_im, gsparams=gsp1, suppress_warning=True), galsim.OpticalPSF(lam_over_diam=1.0, pupil_plane_im=pupil_plane_im, pupil_angle=10*galsim.degrees, suppress_warning=True)] all_obj_diff(objs) t2 = time.time() print 'time for %s = %.2f'%(funcname(),t2-t1)
def main(argv): """ Make images using model PSFs and galaxy cluster shear: - The galaxies come from a processed COSMOS 2015 Catalog, scaled to match anticipated SuperBIT 2021 observations - The galaxy shape parameters are assigned in a probabilistic way through matching galaxy fluxes and redshifts to similar GalSim-COSMOS galaxies (see A. Gill+ 2021) """ global logger logging.basicConfig(format="%(message)s", level=logging.DEBUG, stream=sys.stdout) logger = logging.getLogger("mock_superbit_data") M = MPIHelper() # Define some parameters we'll use below. sbparams = SuperBITParameters(argv=argv) # Set up the NFWHalo: nfw = galsim.NFWHalo(mass=sbparams.mass, conc=sbparams.nfw_conc, redshift=sbparams.nfw_z_halo, omega_m=sbparams.omega_m, omega_lam=sbparams.omega_lam) logger.info('Set up NFW halo for lensing') # Read in galaxy catalog, as well as catalog containing # information from COSMOS fits like redshifts, hlr, etc. # cosmos_cat = galsim.COSMOSCatalog(sbparams.cat_file_name, dir=sbparams.cosmosdir) # fitcat = Table.read(os.path.join(sbparams.cosmosdir, sbparams.fit_file_name)) cosmos_cat = Table.read(os.path.join(sbparams.cosmosdir,sbparams.cat_file_name)) logger.info('Read in %d galaxies from catalog and associated fit info', len(cosmos_cat)) cluster_cat = galsim.COSMOSCatalog(sbparams.cluster_cat_name) logger.debug('Read in %d cluster galaxies from catalog' % cosmos_cat.nobjects) ### Now create PSF. First, define Zernicke polynomial component ### note: aberrations were definined for lam = 550, and close to the ### center of the camera. The PSF degrades at the edge of the FOV lam_over_diam = sbparams.lam * 1.e-9 / sbparams.tel_diam # radians lam_over_diam *= 206265. aberrations = numpy.zeros(38) # Set the initial size. aberrations[0] = 0. # First entry must be zero aberrations[1] = -0.00305127 aberrations[4] = -0.02474205 # Noll index 4 = Defocus aberrations[11] = -0.01544329 # Noll index 11 = Spherical aberrations[22] = 0.00199235 aberrations[26] = 0.00000017 aberrations[37] = 0.00000004 logger.info('Calculated lambda over diam = %f arcsec', lam_over_diam) # will store the Zernicke component of the PSF optics = galsim.OpticalPSF(lam=sbparams.lam,diam=sbparams.tel_diam, obscuration=sbparams.obscuration, nstruts=sbparams.nstruts, strut_angle=sbparams.strut_angle, strut_thick=sbparams.strut_thick, aberrations=aberrations) logger.info('Made telescope PSF profile') ### ### MAKE SIMULATED OBSERVATIONS ### ITERATE n TIMES TO MAKE n SEPARATE IMAGES ### for i in numpy.arange(1,sbparams.nexp+1): # get MPI processes in sync at start of each image M.barrier() #rng = galsim.BaseDeviate(sbparams.noise_seed+i) try: timescale=str(sbparams.exp_time) outname=''.join(['superbit_gaussJitter_',str(i).zfill(3),'.fits']) truth_file_name=''.join([sbparams.outdir, '/truth_gaussJitter_', str(i).zfill(3), '.dat']) file_name = os.path.join(sbparams.outdir, outname) except galsim.errors.GalSimError: print("naming failed, check path") 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','truth_fwhm','truth_mom', 'n','hlr','inclination','scale_h_over_r'] types = [ int, 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(sbparams.image_xsize, sbparams.image_ysize) sky_level = sbparams.exp_time * sbparams.sky_bkg full_image.fill(sky_level) full_image.setOrigin(0,0) # If you wanted to make a non-trivial WCS system, could set theta to a non-zero number theta = 0.0 * galsim.degrees dudx = numpy.cos(theta) * sbparams.pixel_scale dudy = -numpy.sin(theta) * sbparams.pixel_scale dvdx = numpy.sin(theta) * sbparams.pixel_scale dvdy = numpy.cos(theta) * sbparams.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=sbparams.center_ra, dec=sbparams.center_dec) wcs = galsim.TanWCS(affine, sky_center, units=galsim.arcsec) full_image.wcs = wcs ## Now let's read in the PSFEx PSF model, if using. ## We read the image directly into an InterpolatedImage GSObject, ## so we can manipulate it as needed #psf_wcs=wcs #psf = galsim.des.DES_PSFEx(psf_filen,wcs=psf_wcs) #logger.info('Constructed PSF object from PSFEx file') ##### ## Loop over galaxy objects: ##### # get local range to iterate over in this process local_start, local_end = M.mpi_local_range(sbparams.nobj) for k in range(local_start, local_end): time1 = time.time() # The usual random number generator using a different seed for each galaxy. ud = galsim.UniformDeviate(sbparams.galobj_seed+k+1) try: # make single galaxy object stamp,truth = make_a_galaxy(ud=ud,wcs=wcs,affine=affine, cosmos_cat=cosmos_cat,optics=optics,nfw=nfw, sbparams=sbparams) # 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\n', 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.fwhm, truth.mom_size, truth.n, truth.hlr, truth.inclination, truth.scale_h_over_r] truth_catalog.addRow(row) except galsim.errors.GalSimError: logger.info('Galaxy %d has failed, skipping...',k) ##### ### Inject cluster galaxy objects: ##### center_coords = galsim.CelestialCoord(sbparams.center_ra,sbparams.center_dec) centerpix = wcs.toImage(center_coords) # get local range to iterate over in this process local_start, local_end = M.mpi_local_range(sbparams.nclustergal) for k in range(local_start, local_end): time1 = time.time() # The usual random number generator using a different seed for each galaxy. ud = galsim.UniformDeviate(sbparams.cluster_seed+k+1) try: # make single galaxy object cluster_stamp,truth = make_cluster_galaxy(ud=ud,wcs=wcs,affine=affine, centerpix=centerpix, cluster_cat=cluster_cat, optics=optics, sbparams=sbparams) # 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\n', 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.fwhm,truth.mom_size, truth.n, truth.hlr, truth.inclination, truth.scale_h_over_r] truth_catalog.addRow(row) except galsim.errors.GalSimError: logger.info('Cluster galaxy %d has failed, skipping...',k) ##### ### Now repeat process for stars! ##### # get local range to iterate over in this process local_start, local_end = M.mpi_local_range(sbparams.nstars) for k in range(local_start, local_end): time1 = time.time() ud = galsim.UniformDeviate(sbparams.stars_seed+k+1) star_stamp,truth = make_a_star(ud=ud, wcs=wcs, affine=affine, optics=optics, sbparams=sbparams) 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.fwhm,truth.mom_size, truth.n, truth.hlr, truth.inclination, truth.scale_h_over_r] truth_catalog.addRow(row) except galsim.errors.GalSimError: logger.info('Star %d has failed, skipping...',k) # Gather results from MPI processes, reduce to single result on root # Using same names on left and right sides is hiding lots of MPI magic full_image = M.gather(full_image) truth_catalog = M.gather(truth_catalog) if M.is_mpi_root(): full_image = reduce(combine_images, full_image) truth_catalog = reduce(combine_catalogs, truth_catalog) else: # do the adding of noise and writing to disk entirely on root # root and the rest meet again at barrier at start of loop continue # The first thing to do is to make the Gaussian noise uniform across the whole image. # Add dark current logger.info('Adding Dark current') dark_noise = sbparams.dark_current * sbparams.exp_time full_image += dark_noise # Add ccd noise logger.info('Adding CCD noise') noise = galsim.CCDNoise( sky_level=0, gain=1/sbparams.gain, read_noise=sbparams.read_noise) full_image.addNoise(noise) logger.debug('Added noise to final output image') if not os.path.exists(os.path.dirname(file_name)): os.makedirs(os.path.dirname(file_name)) 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 all images') logger.info(' ')
def test_OpticalPSF_aberrations_struts(): """Test the generation of optical aberrations and struts against a known result. """ lod = 0.04 obscuration = 0.3 imsize = 128 # Size of saved images as generated by generate_optics_comparison_images.py myImg = galsim.ImageD(imsize, imsize) # We don't bother running all of these for the regular unit tests, since it adds # ~5s to the test run time on a fast-ish laptop. So only run these when individually # running python test_optics.py. if do_slow_tests: # test defocus savedImg = galsim.fits.read(os.path.join(imgdir, "optics_defocus.fits")) optics = galsim.OpticalPSF(lod, defocus=.5, obscuration=obscuration, oversampling=1) myImg = optics.drawImage(myImg, scale=0.2*lod, use_true_center=True, method='no_pixel') np.testing.assert_array_almost_equal( myImg.array, savedImg.array, 6, err_msg="Optical aberration (defocus) disagrees with expected result") # test astig1 savedImg = galsim.fits.read(os.path.join(imgdir, "optics_astig1.fits")) optics = galsim.OpticalPSF(lod, defocus=.5, astig1=.5, obscuration=obscuration, oversampling=1) myImg = optics.drawImage(myImg, scale=0.2*lod, use_true_center=True, method='no_pixel') np.testing.assert_array_almost_equal( myImg.array, savedImg.array, 6, err_msg="Optical aberration (astig1) disagrees with expected result") # test astig2 savedImg = galsim.fits.read(os.path.join(imgdir, "optics_astig2.fits")) optics = galsim.OpticalPSF(lod, defocus=.5, astig2=.5, obscuration=obscuration, oversampling=1) myImg = optics.drawImage(myImg, scale=0.2*lod, use_true_center=True, method='no_pixel') np.testing.assert_array_almost_equal( myImg.array, savedImg.array, 6, err_msg="Optical aberration (astig2) disagrees with expected result") # test coma1 savedImg = galsim.fits.read(os.path.join(imgdir, "optics_coma1.fits")) optics = galsim.OpticalPSF(lod, coma1=.5, obscuration=obscuration, oversampling=1) myImg = optics.drawImage(myImg, scale=0.2*lod, use_true_center=True, method='no_pixel') np.testing.assert_array_almost_equal( myImg.array, savedImg.array, 6, err_msg="Optical aberration (coma1) disagrees with expected result") # test coma2 savedImg = galsim.fits.read(os.path.join(imgdir, "optics_coma2.fits")) optics = galsim.OpticalPSF(lod, coma2=.5, obscuration=obscuration, oversampling=1) myImg = optics.drawImage(myImg, scale=0.2*lod, use_true_center=True, method='no_pixel') np.testing.assert_array_almost_equal( myImg.array, savedImg.array, 6, err_msg="Optical aberration (coma2) disagrees with expected result") # test trefoil1 savedImg = galsim.fits.read(os.path.join(imgdir, "optics_trefoil1.fits")) optics = galsim.OpticalPSF(lod, trefoil1=.5, obscuration=obscuration, oversampling=1) myImg = optics.drawImage(myImg, scale=0.2*lod, use_true_center=True, method='no_pixel') np.testing.assert_array_almost_equal( myImg.array, savedImg.array, 6, err_msg="Optical aberration (trefoil1) disagrees with expected result") # test trefoil2 savedImg = galsim.fits.read(os.path.join(imgdir, "optics_trefoil2.fits")) optics = galsim.OpticalPSF(lod, trefoil2=.5, obscuration=obscuration, oversampling=1) myImg = optics.drawImage(myImg, scale=0.2*lod, use_true_center=True, method='no_pixel') np.testing.assert_array_almost_equal( myImg.array, savedImg.array, 6, err_msg="Optical aberration (trefoil2) disagrees with expected result") # test spherical savedImg = galsim.fits.read(os.path.join(imgdir, "optics_spher.fits")) optics = galsim.OpticalPSF(lod, spher=.5, obscuration=obscuration, oversampling=1) myImg = optics.drawImage(myImg, scale=0.2*lod, use_true_center=True, method='no_pixel') np.testing.assert_array_almost_equal( myImg.array, savedImg.array, 6, err_msg="Optical aberration (spher) disagrees with expected result") # test all aberrations savedImg = galsim.fits.read(os.path.join(imgdir, "optics_all.fits")) optics = galsim.OpticalPSF(lod, defocus=.5, astig1=0.5, astig2=0.3, coma1=0.4, coma2=-0.3, trefoil1=-0.2, trefoil2=0.1, spher=-0.8, obscuration=obscuration, oversampling=1) myImg = optics.drawImage(myImg, scale=0.2*lod, use_true_center=True, method='no_pixel') np.testing.assert_array_almost_equal( myImg.array, savedImg.array, 6, err_msg="Optical aberration (all aberrations) disagrees with expected result") do_pickle(optics, lambda x: x.drawImage(nx=20, ny=20, scale=1.7, method='no_pixel')) do_pickle(optics) # test struts savedImg = galsim.fits.read(os.path.join(imgdir, "optics_struts.fits")) optics = galsim.OpticalPSF( lod, obscuration=obscuration, nstruts=5, strut_thick=0.04, strut_angle=8.*galsim.degrees, astig2=0.04, coma1=-0.07, defocus=0.09, oversampling=1) with assert_raises(TypeError): galsim.OpticalPSF(lod, nstruts=5, strut_thick=0.01, strut_angle=8.) # wrong units do_pickle(optics, lambda x: x.drawImage(nx=20, ny=20, scale=1.7, method='no_pixel')) do_pickle(optics) # Make sure it doesn't have some weird error if strut_angle=0 (should be the easiest case, but # check anyway...) optics_2 = galsim.OpticalPSF( lod, obscuration=obscuration, nstruts=4, strut_thick=0.05, strut_angle=0.*galsim.degrees, astig2=0.04, coma1=-0.07, defocus=0.09, oversampling=1) myImg = optics.drawImage(myImg, scale=0.2*lod, use_true_center=True, method='no_pixel') np.testing.assert_array_almost_equal( myImg.array, savedImg.array, 6, err_msg="Optical PSF (with struts) disagrees with expected result") # These are also the defaults for strut_thick and strut_angle optics_3 = galsim.OpticalPSF( lod, obscuration=obscuration, nstruts=4, astig2=0.04, coma1=-0.07, defocus=0.09, oversampling=1) assert optics_3 == optics_2 do_pickle(optics_3) # make sure it doesn't completely explode when asked to return a PSF with non-circular pupil and # non-zero obscuration optics = galsim.OpticalPSF( lod, obscuration=obscuration, nstruts=5, strut_thick=0.04, strut_angle=8.*galsim.degrees, astig2=0.04, coma1=-0.07, defocus=0.09, circular_pupil=False, oversampling=1) do_pickle(optics, lambda x: x.drawImage(nx=20, ny=20, scale=1.7, method='no_pixel')) do_pickle(optics)
def main(argv): """ Getting reasonably close to including all the principle features of an image from a ground-based telescope: - Use a bulge plus disk model for the galaxy - Both galaxy components are Sersic profiles (n=3.5 and n=1.5 respectively) - Let the PSF have both atmospheric and optical components. - The atmospheric component is a Kolmogorov spectrum. - The optical component has some defocus, coma, and astigmatism. - Add both Poisson noise to the image and Gaussian read noise. - Let the pixels be slightly distorted relative to the sky. """ # We do some fancier logging for demo3, just to demonstrate that we can: # - we log to both stdout and to a log file # - the log file has a lot more (mostly redundant) information logging.basicConfig(format="%(message)s", level=logging.INFO, stream=sys.stdout) if not os.path.isdir('output'): os.mkdir('output') logFile = logging.FileHandler(os.path.join("output", "script3.log")) logFile.setFormatter(logging.Formatter("%(name)s[%(levelname)s] %(asctime)s: %(message)s")) logging.getLogger("demo3").addHandler(logFile) logger = logging.getLogger("demo3") gal_flux = 1.e6 # ADU ("Analog-to-digital units", the units of the numbers on a CCD) bulge_n = 3.5 # bulge_re = 2.3 # arcsec disk_n = 1.5 # disk_r0 = 0.85 # arcsec (corresponds to half_light_radius of ~3.7 arcsec) bulge_frac = 0.3 # gal_q = 0.73 # (axis ratio 0 < q < 1) gal_beta = 23 # degrees (position angle on the sky) atmos_fwhm=2.1 # arcsec atmos_e = 0.13 # atmos_beta = 0.81 # radians opt_defocus=0.53 # wavelengths opt_a1=-0.29 # wavelengths opt_a2=0.12 # wavelengths opt_c1=0.64 # wavelengths opt_c2=-0.33 # wavelengths opt_obscuration=0.3 # linear scale size of secondary mirror obscuration lam = 800 # nm NB: don't use lambda - that's a reserved word. tel_diam = 4. # meters pixel_scale = 0.23 # arcsec / pixel image_size = 64 # n x n pixels wcs_g1 = -0.02 # wcs_g2 = 0.01 # sky_level = 2.5e4 # ADU / arcsec^2 gain = 1.7 # e- / ADU # Note: here we assume 1 photon -> 1 e-, ignoring QE. If you wanted, # you could include the QE factor as part of the gain. read_noise = 0.3 # e- / pixel random_seed = 1314662 logger.info('Starting demo script 3 using:') logger.info(' - Galaxy is bulge plus disk, flux = %.1e',gal_flux) logger.info(' - Bulge is Sersic (n = %.1f, re = %.2f), frac = %.1f', bulge_n,bulge_re,bulge_frac) logger.info(' - Disk is Sersic (n = %.1f, r0 = %.2f), frac = %.1f', disk_n,disk_r0,1-bulge_frac) logger.info(' - Shape is q,beta (%.2f,%.2f deg)', gal_q, gal_beta) logger.info(' - Atmospheric PSF is Kolmogorov with fwhm = %.2f',atmos_fwhm) logger.info(' - Shape is e,beta (%.2f,%.2f rad)', atmos_e, atmos_beta) logger.info(' - Optical PSF has defocus = %.2f, astigmatism = (%.2f,%.2f),', opt_defocus, opt_a1, opt_a2) logger.info(' coma = (%.2f,%.2f), lambda = %.0f nm, D = %.1f m', opt_c1, opt_c2, lam, tel_diam) logger.info(' obscuration linear size = %.1f',opt_obscuration) logger.info(' - pixel scale = %.2f,',pixel_scale) logger.info(' - WCS distortion = (%.2f,%.2f),',wcs_g1,wcs_g2) logger.info(' - Poisson noise (sky level = %.1e, gain = %.1f).',sky_level, gain) logger.info(' - Gaussian read noise (sigma = %.2f).',read_noise) # Initialize the (pseudo-)random number generator that we will be using below. rng = galsim.BaseDeviate(random_seed+1) # Define the galaxy profile. # Normally Sersic profiles are specified by half-light radius, the radius that # encloses half of the total flux. However, for some purposes, it can be # preferable to instead specify the scale radius, where the surface brightness # drops to 1/e of the central peak value. bulge = galsim.Sersic(bulge_n, half_light_radius=bulge_re) disk = galsim.Sersic(disk_n, scale_radius=disk_r0) # Objects may be multiplied by a scalar (which means scaling the flux) and also # added to each other. gal = bulge_frac * bulge + (1-bulge_frac) * disk # Could also have written the following, which does the same thing: # gal = galsim.Add([ bulge.withFlux(bulge_frac) , disk.withFlux(1-bulge_frac) ]) # Both syntaxes work with more than two summands as well. # Set the overall flux of the combined object. gal = gal.withFlux(gal_flux) # Since the total flux of the components was 1, we could also have written: # gal *= gal_flux # The withFlux method will always set the flux to the given value, while `gal *= flux` # will multiply whatever the current flux is by the given factor. # Set the shape of the galaxy according to axis ratio and position angle # Note: All angles in GalSim must have explicit units. Options are: # galsim.radians # galsim.degrees # galsim.arcmin # galsim.arcsec # galsim.hours gal_shape = galsim.Shear(q=gal_q, beta=gal_beta*galsim.degrees) gal = gal.shear(gal_shape) logger.debug('Made galaxy profile') # Define the atmospheric part of the PSF. # Note: the flux here is the default flux=1. atmos = galsim.Kolmogorov(fwhm=atmos_fwhm) # For the PSF shape here, we use ellipticity rather than axis ratio. # And the position angle can be either degrees or radians. Here we chose radians. atmos = atmos.shear(e=atmos_e, beta=atmos_beta*galsim.radians) logger.debug('Made atmospheric PSF profile') # Define the optical part of the PSF: # The first argument of OpticalPSF below is lambda/diam (wavelength of light / telescope # diameter), which needs to be in the same units used to specify the image scale. We are using # arcsec for that, so we have to self-consistently use arcsec here, using the following # calculation: lam_over_diam = lam * 1.e-9 / tel_diam # radians lam_over_diam *= 206265 # arcsec # Note that we could also have made GalSim do the conversion for us if we did not know the right # factor: # lam_over_diam = lam * 1.e-9 / tel_diam * galsim.radians # lam_over_diam = lam_over_diam / galsim.arcsec logger.debug('Calculated lambda over diam = %f arcsec', lam_over_diam) # The rest of the values should be given in units of the wavelength of the incident light. optics = galsim.OpticalPSF(lam_over_diam, defocus = opt_defocus, coma1 = opt_c1, coma2 = opt_c2, astig1 = opt_a1, astig2 = opt_a2, obscuration = opt_obscuration) logger.debug('Made optical PSF profile') # So far, our coordinate transformation between image and sky coordinates has been just a # scaling of the units between pixels and arcsec, which we have defined as the "pixel scale". # This is fine for many purposes, so we have made it easy to treat the coordinate systems # this way via the `scale` parameter to commands like drawImage. However, in general, the # transformation between the two coordinate systems can be more complicated than that, # including distortions, rotations, variation in pixel size, and so forth. GalSim can # model a number of different "World Coordinate System" (WCS) transformations. See the # docstring for BaseWCS for more information. # In this case, we use a WCS that includes a distortion (specified as g1,g2 in this case), # which we call a ShearWCS. wcs = galsim.ShearWCS(scale=pixel_scale, shear=galsim.Shear(g1=wcs_g1, g2=wcs_g2)) logger.debug('Made the WCS') # Next we will convolve the components in world coordinates. psf = galsim.Convolve([atmos, optics]) final = galsim.Convolve([psf, gal]) logger.debug('Convolved components into final profile') # This time we specify a particular size for the image rather than let GalSim # choose the size automatically. GalSim has several kinds of images that it can use: # ImageF uses 32-bit floats (like a C float, aka numpy.float32) # ImageD uses 64-bit floats (like a C double, aka numpy.float64) # ImageS uses 16-bit integers (usually like a C short, aka numpy.int16) # ImageI uses 32-bit integers (usually like a C int, aka numpy.int32) # If you let the GalSim drawImage command create the image for you, it will create an ImageF. # However, you can make a different type if you prefer. In this case, we still use # ImageF, since 32-bit floats are fine. We just want to set the size explicitly. image = galsim.ImageF(image_size, image_size) # Draw the image with the given WCS. Note that we use wcs rather than scale when the # WCS is more complicated than just a pixel scale. final.drawImage(image=image, wcs=wcs) # Also draw the effective PSF by itself and the optical PSF component alone. image_epsf = galsim.ImageF(image_size, image_size) psf.drawImage(image_epsf, wcs=wcs) # We also draw the optical part of the PSF at its own Nyquist-sampled pixel size # in order to better see the features of the (highly structured) profile. # In this case, we draw a "surface brightness image" using method='sb'. Rather than # integrate the flux over the area of each pixel, this method just samples the surface # brightness value at the locations of the pixel centers. We will encounter a few other # drawing methods as we go through this sequence of demos. cf. demos 7, 8, 10, and 11. image_opticalpsf = optics.drawImage(method='sb') logger.debug('Made image of the profile') # Add a constant sky level to the image. image += sky_level * pixel_scale**2 # This time, we use CCDNoise to model the real noise in a CCD image. It takes a sky level, # gain, and read noise, so it can be a bit more realistic than the simpler GaussianNoise # or PoissonNoise that we used in demos 1 and 2. # # The gain is in units of e-/ADU. Technically, one should also account for quantum efficiency # (QE) of the detector. An ideal CCD has one electron per incident photon, but real CCDs have # QE less than 1, so not every photon triggers an electron. We are essentially folding # the quantum efficiency (and filter transmission and anything else like that) into the gain. # The read_noise value is given as e-/pixel. This is modeled as a pure Gaussian noise # added to the image after applying the pure Poisson noise. image.addNoise(galsim.CCDNoise(rng, gain=gain, read_noise=read_noise)) # Subtract off the sky. image -= sky_level * pixel_scale**2 logger.debug('Added Gaussian and Poisson noise') # Write the images to files. file_name = os.path.join('output', 'demo3.fits') file_name_epsf = os.path.join('output','demo3_epsf.fits') file_name_opticalpsf = os.path.join('output','demo3_opticalpsf.fits') image.write(file_name) image_epsf.write(file_name_epsf) image_opticalpsf.write(file_name_opticalpsf) logger.info('Wrote image to %r', file_name) logger.info('Wrote effective PSF image to %r', file_name_epsf) logger.info('Wrote optics-only PSF image (Nyquist sampled) to %r', file_name_opticalpsf) # Check that the HSM package, which is bundled with GalSim, finds a good estimate # of the shear. results = galsim.hsm.EstimateShear(image, image_epsf) logger.info('HSM reports that the image has observed shape and size:') logger.info(' e1 = %.3f, e2 = %.3f, sigma = %.3f (pixels)', results.observed_shape.e1, results.observed_shape.e2, results.moments_sigma) logger.info('When carrying out Regaussianization PSF correction, HSM reports') logger.info(' e1, e2 = %.3f, %.3f', results.corrected_e1, results.corrected_e2) logger.info('Expected values in the limit that noise and non-Gaussianity are negligible:') # Convention for shear addition is to apply the second term initially followed by the first. # So this needs to be the WCS shear + the galaxy shape in that order. total_shape = galsim.Shear(g1=wcs_g1, g2=wcs_g2) + gal_shape logger.info(' e1, e2 = %.3f, %.3f', total_shape.e1, total_shape.e2)
def test_OpticalPSF_aberrations_kwargs(): """Test that OpticalPSF aberrations kwarg works just like specifying aberrations. """ # Make an OpticalPSF with direct specification of aberrations. lod = 0.04 obscuration = 0.3 opt1 = galsim.OpticalPSF(lod, obscuration=obscuration, defocus=0.1, coma1=-0.1, coma2=0.3) # Now make it with an aberrations list. (Note: should work with len < 12) aberrations = np.zeros(9) aberrations[4] = 0.1 aberrations[7] = -0.1 aberrations[8] = 0.3 opt2 = galsim.OpticalPSF(lod, obscuration=obscuration, aberrations=aberrations) # Make sure they agree. np.testing.assert_array_equal( opt1.drawImage(scale=0.2*lod, method='no_pixel').array, opt2.drawImage(scale=0.2*lod, method='no_pixel').array, err_msg="Optical PSF depends on how aberrations are specified (4,8,11)") # Repeat with all aberrations up to index 11, using a regular list, not a numpy array opt1 = galsim.OpticalPSF(lod, defocus=.5, astig1=0.5, astig2=0.3, coma1=0.4, coma2=-0.3, trefoil1=-0.2, trefoil2=0.1, spher=-0.8, obscuration=obscuration) aberrations = [ 0.0 ] * 4 + [ 0.5, 0.5, 0.3, 0.4, -0.3, -0.2, 0.1, -0.8 ] opt2 = galsim.OpticalPSF(lod, obscuration=obscuration, aberrations=aberrations) np.testing.assert_array_equal( opt1.drawImage(scale=0.2*lod, method='no_pixel').array, opt2.drawImage(scale=0.2*lod, method='no_pixel').array, err_msg="Optical PSF depends on how aberrations are specified (full list)") do_pickle(opt2, lambda x: x.drawImage(nx=20, ny=20, scale=0.01, method='no_pixel')) do_pickle(opt2) # Also, check for proper response to weird inputs. # aberrations must be a list or an array with assert_raises(TypeError): galsim.OpticalPSF(lod, aberrations=0.3) # It must have at least 2 elements with assert_raises(ValueError): galsim.OpticalPSF(lod, aberrations=[0.0]) with assert_raises(ValueError): galsim.OpticalPSF(lod, aberrations=[]) # 2 zeros is equivalent to None assert galsim.OpticalPSF(lod, aberrations=[0, 0]) == galsim.OpticalPSF(lod) # The first element must be 0. (Just a warning!) with assert_warns(galsim.GalSimWarning): galsim.OpticalPSF(lod, aberrations=[0.3]*8) # Cannot provide both aberrations and specific ones by name. with assert_raises(TypeError): galsim.OpticalPSF(lod, aberrations=np.zeros(8), defocus=-0.12)
def main(argv): """ Make a fits image cube where each frame has two images of the same galaxy drawn with regular FFT convolution and with photon shooting. We do this for 5 different PSFs and 5 different galaxies, each with 4 different (random) fluxes, sizes, and shapes. """ logging.basicConfig(format="%(message)s", level=logging.INFO, stream=sys.stdout) logger = logging.getLogger("demo7") # To turn off logging: #logger.propagate = False # Define some parameters we'll use below. # Make output directory if not already present. if not os.path.isdir('output'): os.mkdir('output') file_name = os.path.join('output', 'cube_phot.fits.gz') random_seed = 553728 sky_level = 1.e4 # ADU / arcsec^2 pixel_scale = 0.28 # arcsec nx = 64 ny = 64 gal_flux_min = 1.e4 # Range for galaxy flux gal_flux_max = 1.e5 gal_hlr_min = 0.3 # arcsec gal_hlr_max = 1.3 # arcsec gal_e_min = 0. # Range for ellipticity gal_e_max = 0.8 psf_fwhm = 0.65 # arcsec # This script is set up as a comparison between using FFTs for doing the convolutions and # shooting photons. The two methods have trade-offs in speed and accuracy which vary # with the kind of profile being drawn and the S/N of the object, among other factors. # In addition, for each method, there are a number of parameters GalSim uses that control # aspects of the calculation that further affect the speed and accuracy. # # We encapsulate these parameters with an object called GSParams. The default values # are intended to be accurate enough for normal precision shear tests, without sacrificing # too much speed. # # Any PSF or galaxy object can be given a gsparams argument on construction that can # have different values to make the calculation more or less accurate (typically trading # off for speed or memory). # # In this script, we adjust some of the values slightly, just to show you how it works. # You could play around with these values and see what effect they have on the drawn images. # Usually, it requires a pretty drastic change in these parameters for you to be able to # notice the difference by eye. But subtle effects that may impact the shapes of galaxies # can happen well before then. # Type help(galsim.GSParams) for the complete list of parameters and more detailed # documentation, including the default values for each parameter. gsparams = galsim.GSParams( alias_threshold= 1.e-2, # maximum fractional flux that may be aliased around edge of FFT maxk_threshold= 2.e-3, # k-values less than this may be excluded off edge of FFT xvalue_accuracy= 1.e-4, # approximations in real space aim to be this accurate kvalue_accuracy= 1.e-4, # approximations in fourier space aim to be this accurate shoot_accuracy= 1.e-4, # approximations in photon shooting aim to be this accurate minimum_fft_size=64) # minimum size of ffts logger.info('Starting demo script 7') # Make the pixel: pix = galsim.Pixel(xw=pixel_scale) # Make the PSF profiles: psf1 = galsim.Gaussian(fwhm=psf_fwhm, gsparams=gsparams) psf2 = galsim.Moffat(fwhm=psf_fwhm, beta=2.4, gsparams=gsparams) psf3_inner = galsim.Gaussian(fwhm=psf_fwhm, flux=0.8, gsparams=gsparams) psf3_outer = galsim.Gaussian(fwhm=2 * psf_fwhm, flux=0.2, gsparams=gsparams) psf3 = psf3_inner + psf3_outer atmos = galsim.Gaussian(fwhm=psf_fwhm, gsparams=gsparams) optics = galsim.OpticalPSF(lam_over_diam=0.6 * psf_fwhm, obscuration=0.4, defocus=0.1, astig1=0.3, astig2=-0.2, coma1=0.2, coma2=0.1, spher=-0.3, gsparams=gsparams) psf4 = galsim.Convolve([atmos, optics ]) # Convolve inherits the gsparams from the first # item in the list. (Or you can supply a gsparams # argument explicitly if you want to override this.) atmos = galsim.Kolmogorov(fwhm=psf_fwhm, gsparams=gsparams) optics = galsim.Airy(lam_over_diam=0.3 * psf_fwhm, gsparams=gsparams) psf5 = galsim.Convolve([atmos, optics]) psfs = [psf1, psf2, psf3, psf4, psf5] psf_names = [ "Gaussian", "Moffat", "Double Gaussian", "OpticalPSF", "Kolmogorov * Airy" ] psf_times = [0, 0, 0, 0, 0] psf_fft_times = [0, 0, 0, 0, 0] psf_phot_times = [0, 0, 0, 0, 0] # Make the galaxy profiles: gal1 = galsim.Gaussian(half_light_radius=1, gsparams=gsparams) gal2 = galsim.Exponential(half_light_radius=1, gsparams=gsparams) gal3 = galsim.DeVaucouleurs(half_light_radius=1, gsparams=gsparams) gal4 = galsim.Sersic(half_light_radius=1, n=2.5, gsparams=gsparams) bulge = galsim.Sersic(half_light_radius=0.7, n=3.2, gsparams=gsparams) disk = galsim.Sersic(half_light_radius=1.2, n=1.5, gsparams=gsparams) gal5 = 0.4 * bulge + 0.6 * disk # Net half-light radius is only approximate for this one. gals = [gal1, gal2, gal3, gal4, gal5] gal_names = [ "Gaussian", "Exponential", "Devaucouleurs", "n=2.5 Sersic", "Bulge + Disk" ] gal_times = [0, 0, 0, 0, 0] gal_fft_times = [0, 0, 0, 0, 0] gal_phot_times = [0, 0, 0, 0, 0] # Other times to keep track of: setup_times = 0 fft_times = 0 phot_times = 0 noise_times = 0 # Loop over combinations of psf, gal, and make 4 random choices for flux, size, shape. all_images = [] k = 0 for ipsf in range(len(psfs)): psf = psfs[ipsf] psf_name = psf_names[ipsf] for igal in range(len(gals)): gal = gals[igal] gal_name = gal_names[igal] for i in range(4): logger.debug('Start work on image %d', i) t1 = time.time() # Initialize the random number generator we will be using. rng = galsim.UniformDeviate(random_seed + k) # Get a new copy, we'll want to keep the original unmodified. gal1 = gal.copy() # Generate random variates: flux = rng() * (gal_flux_max - gal_flux_min) + gal_flux_min gal1.setFlux(flux) hlr = rng() * (gal_hlr_max - gal_hlr_min) + gal_hlr_min gal1.applyDilation(hlr) beta_ellip = rng() * 2 * math.pi * galsim.radians ellip = rng() * (gal_e_max - gal_e_min) + gal_e_min gal_shape = galsim.Shear(e=ellip, beta=beta_ellip) gal1.applyShear(gal_shape) # Build the final object by convolving the galaxy, PSF and pixel response. final = galsim.Convolve([psf, pix, gal1]) # For photon shooting, need a version without the pixel (see below). final_nopix = galsim.Convolve([psf, gal1]) # Create the large, double width output image image = galsim.ImageF(2 * nx + 2, ny) # Rather than provide a dx= argument to the draw commands, we can also # set the pixel scale in the image itself with setScale. image.setScale(pixel_scale) # Assign the following two "ImageViews", fft_image and phot_image. # Using the syntax below, these are views into the larger image. # Changes/additions to the sub-images referenced by the views are automatically # reflected in the original image. fft_image = image[galsim.BoundsI(1, nx, 1, ny)] phot_image = image[galsim.BoundsI(nx + 3, 2 * nx + 2, 1, ny)] logger.debug( ' Read in training sample galaxy and PSF from file') t2 = time.time() # Draw the profile final.draw(fft_image) logger.debug( ' Drew fft image. Total drawn flux = %f. .flux = %f', fft_image.array.sum(), final.getFlux()) t3 = time.time() # Add Poisson noise sky_level_pixel = sky_level * pixel_scale**2 fft_image.addNoise( galsim.PoissonNoise(rng, sky_level=sky_level_pixel)) t4 = time.time() # The next two lines are just to get the output from this demo script # to match the output from the parsing of demo7.yaml. rng = galsim.UniformDeviate(random_seed + k) rng() rng() rng() rng() # Repeat for photon shooting image. # Photon shooting automatically convolves by the pixel, so we've made sure not # to include it in the profile! final_nopix.drawShoot(phot_image, max_extra_noise=sky_level_pixel / 100, rng=rng) t5 = time.time() # For photon shooting, galaxy already has Poisson noise, so we want to make # sure not to add that noise again! Thus, we just add sky noise, which # is Poisson with the mean = sky_level_pixel pd = galsim.PoissonDeviate(rng, mean=sky_level_pixel) # DeviateNoise just adds the action of the given deviate to every pixel. phot_image.addNoise(galsim.DeviateNoise(pd)) # For PoissonDeviate, the mean is not zero, so for a background-subtracted # image, we need to subtract the mean back off when we are done. phot_image -= sky_level_pixel logger.debug( ' Added Poisson noise. Image fluxes are now %f and %f', fft_image.array.sum(), phot_image.array.sum()) t6 = time.time() # Store that into the list of all images all_images += [image] k = k + 1 logger.info( '%d: %s * %s, flux = %.2e, hlr = %.2f, ellip = (%.2f,%.2f)', k, gal_name, psf_name, flux, hlr, gal_shape.getE1(), gal_shape.getE2()) logger.debug(' Times: %f, %f, %f, %f, %f', t2 - t1, t3 - t2, t4 - t3, t5 - t4, t6 - t5) psf_times[ipsf] += t6 - t1 psf_fft_times[ipsf] += t3 - t2 psf_phot_times[ipsf] += t5 - t4 gal_times[igal] += t6 - t1 gal_fft_times[igal] += t3 - t2 gal_phot_times[igal] += t5 - t4 setup_times += t2 - t1 fft_times += t3 - t2 phot_times += t5 - t4 noise_times += t4 - t3 + t6 - t5 logger.info('Done making images of galaxies') logger.info('') logger.info('Some timing statistics:') logger.info(' Total time for setup steps = %f', setup_times) logger.info(' Total time for regular fft drawing = %f', fft_times) logger.info(' Total time for photon shooting = %f', phot_times) logger.info(' Total time for adding noise = %f', noise_times) logger.info('') logger.info('Breakdown by PSF type:') for ipsf in range(len(psfs)): logger.info(' %s: Total time = %f (fft: %f, phot: %f)', psf_names[ipsf], psf_times[ipsf], psf_fft_times[ipsf], psf_phot_times[ipsf]) logger.info('') logger.info('Breakdown by Galaxy type:') for igal in range(len(gals)): logger.info(' %s: Total time = %f (fft: %f, phot: %f)', gal_names[igal], gal_times[igal], gal_fft_times[igal], gal_phot_times[igal]) logger.info('') # Now write the image to disk. # With any write command, you can optionally compress the file using several compression # schemes: # 'gzip' uses gzip on the full output file. # 'bzip2' uses bzip2 on the full output file. # 'rice' uses rice compression on the image, leaving the fits headers readable. # 'gzip_tile' uses gzip in tiles on the output image, leaving the fits headers readable. # 'hcompress' uses hcompress on the image, but it is only valid for 2-d data, so it # doesn't work for writeCube. # 'plio' uses plio on the image, but it is only valid for positive integer data. # Furthermore, the first three have standard filename extensions associated with them, # so if you don't specify a compression, but the filename ends with '.gz', '.bz2' or '.fz', # the corresponding compression will be selected automatically. # In other words, the `compression='gzip'` specification is actually optional here: galsim.fits.writeCube(all_images, file_name, compression='gzip') logger.info('Wrote fft image to fits data cube %r', file_name)
def test_OpticalPSF_flux_scaling(): """Test flux scaling for OpticalPSF. """ # OpticalPSF test params (only a selection) test_flux = 1.8 test_loD = 1.9 test_obscuration = 0.32 test_defocus = -0.7 test_astig1 = 0.03 test_astig2 = -0.04 test_oversampling = 1.3 test_pad_factor = 1.7 # decimal point to go to for parameter value comparisons param_decimal = 12 # init obj = galsim.OpticalPSF( lam_over_diam=test_loD, oversampling=test_oversampling,pad_factor=test_pad_factor, defocus=test_defocus, astig1=test_astig1, astig2=test_astig2, flux=test_flux) obj *= 2. np.testing.assert_almost_equal( obj.flux, test_flux * 2., decimal=param_decimal, err_msg="Flux param inconsistent after __imul__.") obj = galsim.OpticalPSF( lam_over_diam=test_loD, oversampling=test_oversampling, pad_factor=test_pad_factor, defocus=test_defocus, astig1=test_astig1, astig2=test_astig2, flux=test_flux) obj /= 2. np.testing.assert_almost_equal( obj.flux, test_flux / 2., decimal=param_decimal, err_msg="Flux param inconsistent after __idiv__.") obj = galsim.OpticalPSF( lam_over_diam=test_loD, oversampling=test_oversampling, pad_factor=test_pad_factor, defocus=test_defocus, astig1=test_astig1, astig2=test_astig2, flux=test_flux) obj2 = obj * 2. # First test that original obj is unharmed... np.testing.assert_almost_equal( obj.flux, test_flux, decimal=param_decimal, err_msg="Flux param inconsistent after __rmul__ (original).") # Then test new obj2 flux np.testing.assert_almost_equal( obj2.flux, test_flux * 2., decimal=param_decimal, err_msg="Flux param inconsistent after __rmul__ (result).") obj = galsim.OpticalPSF( lam_over_diam=test_loD, oversampling=test_oversampling, pad_factor=test_pad_factor, defocus=test_defocus, astig1=test_astig1, astig2=test_astig2, flux=test_flux) obj2 = 2. * obj # First test that original obj is unharmed... np.testing.assert_almost_equal( obj.flux, test_flux, decimal=param_decimal, err_msg="Flux param inconsistent after __mul__ (original).") # Then test new obj2 flux np.testing.assert_almost_equal( obj2.flux, test_flux * 2., decimal=param_decimal, err_msg="Flux param inconsistent after __mul__ (result).") obj = galsim.OpticalPSF( lam_over_diam=test_loD, oversampling=test_oversampling, pad_factor=test_pad_factor, defocus=test_defocus, astig1=test_astig1, astig2=test_astig2, flux=test_flux) obj2 = obj / 2. # First test that original obj is unharmed... np.testing.assert_almost_equal( obj.flux, test_flux, decimal=param_decimal, err_msg="Flux param inconsistent after __div__ (original).") # Then test new obj2 flux np.testing.assert_almost_equal( obj2.flux, test_flux / 2., decimal=param_decimal, err_msg="Flux param inconsistent after __div__ (result).")
def test_flip(): """Test several ways to flip a profile """ # The Shapelet profile has the advantage of being fast and not circularly symmetric, so # it is a good test of the actual code for doing the flips (in SBTransform). # But since the bug Rachel reported in #645 was actually in SBInterpolatedImage # (one calculation implicitly assumed dx > 0), it seems worthwhile to run through all the # classes to make sure we hit everything with negative steps for dx and dy. prof_list = [ galsim.Shapelet(sigma=0.17, order=2, bvec=[1.7, 0.01,0.03, 0.29, 0.33, -0.18]), ] if __name__ == "__main__": image_dir = './real_comparison_images' catalog_file = 'test_catalog.fits' rgc = galsim.RealGalaxyCatalog(catalog_file, dir=image_dir) # Some of these are slow, so only do the Shapelet test as part of the normal unit tests. prof_list += [ galsim.Airy(lam_over_diam=0.17, flux=1.7), galsim.Airy(lam_over_diam=0.17, obscuration=0.2, flux=1.7), # Box gets rendered with real-space convolution. The default accuracy isn't quite # enough to get the flip to match at 6 decimal places. galsim.Box(0.17, 0.23, flux=1.7, gsparams=galsim.GSParams(realspace_relerr=1.e-6)), # Without being convolved by anything with a reasonable k cutoff, this needs # a very large fft. galsim.DeVaucouleurs(half_light_radius=0.17, flux=1.7), # I don't really understand why this needs a lower maxk_threshold to work, but # without it, the k-space tests fail. galsim.Exponential(scale_radius=0.17, flux=1.7, gsparams=galsim.GSParams(maxk_threshold=1.e-4)), galsim.Gaussian(sigma=0.17, flux=1.7), galsim.Kolmogorov(fwhm=0.17, flux=1.7), galsim.Moffat(beta=2.5, fwhm=0.17, flux=1.7), galsim.Moffat(beta=2.5, fwhm=0.17, flux=1.7, trunc=0.82), galsim.OpticalPSF(lam_over_diam=0.17, obscuration=0.2, nstruts=6, coma1=0.2, coma2=0.5, defocus=-0.1, flux=1.7), # Like with Box, we need to increase the real-space convolution accuracy. # This time lowering both relerr and abserr. galsim.Pixel(0.23, flux=1.7, gsparams=galsim.GSParams(realspace_relerr=1.e-6, realspace_abserr=1.e-8)), # Note: RealGalaxy should not be rendered directly because of the deconvolution. # Here we convolve it by a Gaussian that is slightly larger than the original PSF. galsim.Convolve([ galsim.RealGalaxy(rgc, index=0, flux=1.7), # "Real" RealGalaxy galsim.Gaussian(sigma=0.08) ]), galsim.Convolve([ galsim.RealGalaxy(rgc, index=1, flux=1.7), # "Fake" RealGalaxy galsim.Gaussian(sigma=0.08) ]), # (cf. test_real.py) galsim.Spergel(nu=-0.19, half_light_radius=0.17, flux=1.7), galsim.Spergel(nu=0., half_light_radius=0.17, flux=1.7), galsim.Spergel(nu=0.8, half_light_radius=0.17, flux=1.7), galsim.Sersic(n=2.3, half_light_radius=0.17, flux=1.7), galsim.Sersic(n=2.3, half_light_radius=0.17, flux=1.7, trunc=0.82), # The shifts here caught a bug in how SBTransform handled the recentering. # Two of the shifts (0.125 and 0.375) lead back to 0.0 happening on an integer # index, which now works correctly. galsim.Sum([ galsim.Gaussian(sigma=0.17, flux=1.7).shift(-0.2,0.125), galsim.Exponential(scale_radius=0.23, flux=3.1).shift(0.375,0.23)]), galsim.TopHat(0.23, flux=1.7), # Box and Pixel use real-space convolution. Convolve with a Gaussian to get fft. galsim.Convolve([ galsim.Box(0.17, 0.23, flux=1.7).shift(-0.2,0.1), galsim.Gaussian(sigma=0.09) ]), galsim.Convolve([ galsim.TopHat(0.17, flux=1.7).shift(-0.275,0.125), galsim.Gaussian(sigma=0.09) ]), # Test something really crazy with several layers worth of transformations galsim.Convolve([ galsim.Sum([ galsim.Gaussian(sigma=0.17, flux=1.7).shear(g1=0.1,g2=0.2).shift(2,3), galsim.Kolmogorov(fwhm=0.33, flux=3.9).transform(0.31,0.19,-0.23,0.33) * 88., galsim.Box(0.11, 0.44, flux=4).rotate(33 * galsim.degrees) / 1.9 ]).shift(-0.3,1), galsim.AutoConvolve(galsim.TopHat(0.5).shear(g1=0.3,g2=0)).rotate(3*galsim.degrees), (galsim.AutoCorrelate(galsim.Box(0.2, 0.3)) * 11).shift(3,2).shift(2,-3) * 0.31 ]).shift(0,0).transform(0,-1,-1,0).shift(-1,1) ] s = galsim.Shear(g1=0.11, g2=-0.21) s1 = galsim.Shear(g1=0.11, g2=0.21) # Appropriate for the flips around x and y axes s2 = galsim.Shear(g1=-0.11, g2=-0.21) # Appropriate for the flip around x=y # Also use shears with just a g1 to get dx != dy, but dxy, dyx = 0. q = galsim.Shear(g1=0.11, g2=0.) q1 = galsim.Shear(g1=0.11, g2=0.) # Appropriate for the flips around x and y axes q2 = galsim.Shear(g1=-0.11, g2=0.) # Appropriate for the flip around x=y decimal=6 # Oddly, these aren't as precise as I would have expected. # Even when we only go to this many digits of accuracy, the Exponential needed # a lower than default value for maxk_threshold. im = galsim.ImageD(16,16, scale=0.05) for prof in prof_list: print('prof = ',prof) # Not all profiles are expected to have a max_sb value close to the maximum pixel value, # so mark the ones where we don't want to require this to be true. close_maxsb = True name = str(prof) if ('DeVauc' in name or 'Sersic' in name or 'Spergel' in name or 'Optical' in name or 'shift' in name): close_maxsb = False # Make sure we hit all 4 fill functions. # image_x uses fillXValue with izero, jzero # image_x1 uses fillXValue with izero, jzero, and unequal dx,dy # image_x2 uses fillXValue with dxy, dyx # image_k uses fillKValue with izero, jzero # image_k1 uses fillKValue with izero, jzero, and unequal dx,dy # image_k2 uses fillKValue with dxy, dyx image_x = prof.drawImage(image=im.copy(), method='no_pixel') image_x1 = prof.shear(q).drawImage(image=im.copy(), method='no_pixel') image_x2 = prof.shear(s).drawImage(image=im.copy(), method='no_pixel') image_k = prof.drawImage(image=im.copy()) image_k1 = prof.shear(q).drawImage(image=im.copy()) image_k2 = prof.shear(s).drawImage(image=im.copy()) if close_maxsb: np.testing.assert_allclose( image_x.array.max(), prof.max_sb*im.scale**2, rtol=0.2, err_msg="max_sb did not match maximum pixel value") np.testing.assert_allclose( image_x1.array.max(), prof.shear(q).max_sb*im.scale**2, rtol=0.2, err_msg="max_sb did not match maximum pixel value") np.testing.assert_allclose( image_x2.array.max(), prof.shear(s).max_sb*im.scale**2, rtol=0.2, err_msg="max_sb did not match maximum pixel value") # Flip around y axis (i.e. x -> -x) flip1 = prof.transform(-1, 0, 0, 1) image2_x = flip1.drawImage(image=im.copy(), method='no_pixel') np.testing.assert_array_almost_equal( image_x.array, image2_x.array[:,::-1], decimal=decimal, err_msg="Flipping image around y-axis failed x test") image2_x1 = flip1.shear(q1).drawImage(image=im.copy(), method='no_pixel') np.testing.assert_array_almost_equal( image_x1.array, image2_x1.array[:,::-1], decimal=decimal, err_msg="Flipping image around y-axis failed x1 test") image2_x2 = flip1.shear(s1).drawImage(image=im.copy(), method='no_pixel') np.testing.assert_array_almost_equal( image_x2.array, image2_x2.array[:,::-1], decimal=decimal, err_msg="Flipping image around y-axis failed x2 test") image2_k = flip1.drawImage(image=im.copy()) np.testing.assert_array_almost_equal( image_k.array, image2_k.array[:,::-1], decimal=decimal, err_msg="Flipping image around y-axis failed k test") image2_k1 = flip1.shear(q1).drawImage(image=im.copy()) np.testing.assert_array_almost_equal( image_k1.array, image2_k1.array[:,::-1], decimal=decimal, err_msg="Flipping image around y-axis failed k1 test") image2_k2 = flip1.shear(s1).drawImage(image=im.copy()) np.testing.assert_array_almost_equal( image_k2.array, image2_k2.array[:,::-1], decimal=decimal, err_msg="Flipping image around y-axis failed k2 test") if close_maxsb: np.testing.assert_allclose( image2_x.array.max(), flip1.max_sb*im.scale**2, rtol=0.2, err_msg="max_sb did not match maximum pixel value") np.testing.assert_allclose( image2_x1.array.max(), flip1.shear(q).max_sb*im.scale**2, rtol=0.2, err_msg="max_sb did not match maximum pixel value") np.testing.assert_allclose( image2_x2.array.max(), flip1.shear(s).max_sb*im.scale**2, rtol=0.2, err_msg="max_sb did not match maximum pixel value") # Flip around x axis (i.e. y -> -y) flip2 = prof.transform(1, 0, 0, -1) image2_x = flip2.drawImage(image=im.copy(), method='no_pixel') np.testing.assert_array_almost_equal( image_x.array, image2_x.array[::-1,:], decimal=decimal, err_msg="Flipping image around x-axis failed x test") image2_x1 = flip2.shear(q1).drawImage(image=im.copy(), method='no_pixel') np.testing.assert_array_almost_equal( image_x1.array, image2_x1.array[::-1,:], decimal=decimal, err_msg="Flipping image around x-axis failed x1 test") image2_x2 = flip2.shear(s1).drawImage(image=im.copy(), method='no_pixel') np.testing.assert_array_almost_equal( image_x2.array, image2_x2.array[::-1,:], decimal=decimal, err_msg="Flipping image around x-axis failed x2 test") image2_k = flip2.drawImage(image=im.copy()) np.testing.assert_array_almost_equal( image_k.array, image2_k.array[::-1,:], decimal=decimal, err_msg="Flipping image around x-axis failed k test") image2_k1 = flip2.shear(q1).drawImage(image=im.copy()) np.testing.assert_array_almost_equal( image_k1.array, image2_k1.array[::-1,:], decimal=decimal, err_msg="Flipping image around x-axis failed k1 test") image2_k2 = flip2.shear(s1).drawImage(image=im.copy()) np.testing.assert_array_almost_equal( image_k2.array, image2_k2.array[::-1,:], decimal=decimal, err_msg="Flipping image around x-axis failed k2 test") if close_maxsb: np.testing.assert_allclose( image2_x.array.max(), flip2.max_sb*im.scale**2, rtol=0.2, err_msg="max_sb did not match maximum pixel value") np.testing.assert_allclose( image2_x1.array.max(), flip2.shear(q).max_sb*im.scale**2, rtol=0.2, err_msg="max_sb did not match maximum pixel value") np.testing.assert_allclose( image2_x2.array.max(), flip2.shear(s).max_sb*im.scale**2, rtol=0.2, err_msg="max_sb did not match maximum pixel value") # Flip around x=y (i.e. y -> x, x -> y) flip3 = prof.transform(0, 1, 1, 0) image2_x = flip3.drawImage(image=im.copy(), method='no_pixel') np.testing.assert_array_almost_equal( image_x.array, np.transpose(image2_x.array), decimal=decimal, err_msg="Flipping image around x=y failed x test") image2_x1 = flip3.shear(q2).drawImage(image=im.copy(), method='no_pixel') np.testing.assert_array_almost_equal( image_x1.array, np.transpose(image2_x1.array), decimal=decimal, err_msg="Flipping image around x=y failed x1 test") image2_x2 = flip3.shear(s2).drawImage(image=im.copy(), method='no_pixel') np.testing.assert_array_almost_equal( image_x2.array, np.transpose(image2_x2.array), decimal=decimal, err_msg="Flipping image around x=y failed x2 test") image2_k = flip3.drawImage(image=im.copy()) np.testing.assert_array_almost_equal( image_k.array, np.transpose(image2_k.array), decimal=decimal, err_msg="Flipping image around x=y failed k test") image2_k1 = flip3.shear(q2).drawImage(image=im.copy()) np.testing.assert_array_almost_equal( image_k1.array, np.transpose(image2_k1.array), decimal=decimal, err_msg="Flipping image around x=y failed k1 test") image2_k2 = flip3.shear(s2).drawImage(image=im.copy()) np.testing.assert_array_almost_equal( image_k2.array, np.transpose(image2_k2.array), decimal=decimal, err_msg="Flipping image around x=y failed k2 test") if close_maxsb: np.testing.assert_allclose( image2_x.array.max(), flip3.max_sb*im.scale**2, rtol=0.2, err_msg="max_sb did not match maximum pixel value") np.testing.assert_allclose( image2_x1.array.max(), flip3.shear(q).max_sb*im.scale**2, rtol=0.2, err_msg="max_sb did not match maximum pixel value") np.testing.assert_allclose( image2_x2.array.max(), flip3.shear(s).max_sb*im.scale**2, rtol=0.2, err_msg="max_sb did not match maximum pixel value") do_pickle(prof, lambda x: x.drawImage(image=im.copy(), method='no_pixel')) do_pickle(flip1, lambda x: x.drawImage(image=im.copy(), method='no_pixel')) do_pickle(flip2, lambda x: x.drawImage(image=im.copy(), method='no_pixel')) do_pickle(flip3, lambda x: x.drawImage(image=im.copy(), method='no_pixel')) do_pickle(prof) do_pickle(flip1) do_pickle(flip2) do_pickle(flip3)
def test_OpticalPSF_pupil_plane(): """Test the ability to generate a PSF using an image of the pupil plane. """ # Test case: lam/diam=0.12, obscuration=0.18, 4 struts of the default width and with rotation # from the vertical of -15 degrees. There are two versions of these tests at different # oversampling levels. # # To (re-)generate the pupil plane images for this test, simply delete # tests/Optics_comparison_images/sample_pupil_rolled.fits and # tests/Optics_comparison_images/sample_pupil_rolled_oversample.fits.gz, # and then rerun this function. Note that these images are also used in test_ne(), so there # may be some racing if this script is tested in parallel before the fits files are regenerated. # First test: should get excellent agreement between that particular OpticalPSF with specified # options and one from loading the pupil plane image. Note that this won't work if you change # the optical PSF parameters, unless you also regenerate the test image. lam_over_diam = 0.12 obscuration = 0.18 nstruts = 4 strut_angle = -15.*galsim.degrees scale = 0.055 ref_psf = galsim.OpticalPSF(lam_over_diam, obscuration=obscuration, nstruts=nstruts, oversampling=pp_oversampling, strut_angle=strut_angle, pad_factor=pp_pad_factor) if os.path.isfile(os.path.join(imgdir, pp_file)): im = galsim.fits.read(os.path.join(imgdir, pp_file)) else: import warnings warnings.warn("Could not find file {0}, so generating it from scratch. This should only " "happen if you intentionally deleted the file in order to regenerate it!" .format(pp_file)) im = galsim.Image(ref_psf._psf.aper.illuminated.astype(float)) im.scale = ref_psf._psf.aper.pupil_plane_scale print('pupil_plane image has scale = ',im.scale) im.write(os.path.join(imgdir, pp_file)) pp_scale = im.scale print('pupil_plane image has scale = ',pp_scale) # For most of the tests, we remove this scale, since for achromatic tests, you don't really # need it, and it is invalid to give lam_over_diam (rather than lam, diam separately) when # there is a specific scale for the pupil plane image. But see the last test below where # we do use lam, diam separately with the input image. im.wcs = None # This implies that the lam_over_diam value is valid. test_psf = galsim.OpticalPSF(lam_over_diam, obscuration=obscuration, oversampling=pp_oversampling, pupil_plane_im=im, pad_factor=pp_pad_factor) im_ref_psf = ref_psf.drawImage(scale=scale) im_test_psf = galsim.ImageD(im_ref_psf.array.shape[0], im_ref_psf.array.shape[1]) im_test_psf = test_psf.drawImage(image=im_test_psf, scale=scale) if pp_test_type == 'image': np.testing.assert_array_almost_equal( im_test_psf.array, im_ref_psf.array, decimal=pp_decimal, err_msg="Inconsistent OpticalPSF image for basic model after loading pupil plane.") else: test_moments = im_test_psf.FindAdaptiveMom() ref_moments = im_ref_psf.FindAdaptiveMom() np.testing.assert_almost_equal( test_moments.moments_sigma, ref_moments.moments_sigma, decimal=pp_decimal, err_msg="Inconsistent OpticalPSF image for basic model after loading pupil plane.") if do_slow_tests: do_pickle(test_psf, lambda x: x.drawImage(nx=20, ny=20, scale=0.07, method='no_pixel')) do_pickle(test_psf) # Make a smaller pupil plane image to test the pickling of this, even without slow tests. factor = 4 if not do_slow_tests else 16 with assert_warns(galsim.GalSimWarning): alt_psf = galsim.OpticalPSF(lam_over_diam, obscuration=obscuration, oversampling=1., pupil_plane_im=im.bin(factor,factor), pad_factor=1.) do_pickle(alt_psf) assert_raises(ValueError, galsim.OpticalPSF, lam_over_diam, pupil_plane_im='pp_file') assert_raises(ValueError, galsim.OpticalPSF, lam_over_diam, pupil_plane_im=im, pupil_plane_scale=pp_scale) assert_raises(ValueError, galsim.OpticalPSF, lam_over_diam, pupil_plane_im=im.view(scale=pp_scale)) # These aren't raised until the image is actually used with assert_raises(ValueError): # not square op = galsim.OpticalPSF(lam_over_diam, pupil_plane_im=galsim.Image(im.array[:-2,:])) op.drawImage() with assert_raises(ValueError): # not even sides op = galsim.OpticalPSF(lam_over_diam, pupil_plane_im=galsim.Image(im.array[:-1,:-1])) op.drawImage() # It is supposed to be able to figure this out even if we *don't* tell it the pad factor. So # make sure that it still works even if we don't tell it that value. test_psf = galsim.OpticalPSF(lam_over_diam, obscuration=obscuration, pupil_plane_im=im, oversampling=pp_oversampling) im_test_psf = galsim.ImageD(im_ref_psf.array.shape[0], im_ref_psf.array.shape[1]) im_test_psf = test_psf.drawImage(image=im_test_psf, scale=scale) if pp_test_type == 'image': np.testing.assert_array_almost_equal( im_test_psf.array, im_ref_psf.array, decimal=pp_decimal, err_msg="Inconsistent OpticalPSF image for basic model after loading pupil plane without " "specifying parameters.") else: test_moments = im_test_psf.FindAdaptiveMom() ref_moments = im_ref_psf.FindAdaptiveMom() np.testing.assert_almost_equal( test_moments.moments_sigma, ref_moments.moments_sigma, decimal=pp_decimal, err_msg="Inconsistent OpticalPSF image for basic model after loading pupil plane without " "specifying parameters.") # Next test (less trivial): Rotate the struts by +27 degrees, and check that agreement is # good. This is making sure that the linear interpolation that is done when rotating does not # result in significant loss of accuracy. rot_angle = 27.*galsim.degrees ref_psf = galsim.OpticalPSF(lam_over_diam, obscuration=obscuration, nstruts=nstruts, strut_angle=strut_angle+rot_angle, oversampling=pp_oversampling, pad_factor=pp_pad_factor) test_psf = galsim.OpticalPSF(lam_over_diam, obscuration=obscuration, pupil_plane_im=im, pupil_angle=rot_angle, oversampling=pp_oversampling, pad_factor=pp_pad_factor) im_ref_psf = ref_psf.drawImage(scale=scale) im_test_psf = galsim.ImageD(im_ref_psf.array.shape[0], im_ref_psf.array.shape[1]) im_test_psf = test_psf.drawImage(image=im_test_psf, scale=scale) # We are slightly less stringent here since it should not be exact. if pp_test_type == 'image': np.testing.assert_array_almost_equal( im_test_psf.array, im_ref_psf.array, decimal=pp_decimal-1, err_msg="Inconsistent OpticalPSF image for rotated model after loading pupil plane.") else: test_moments = im_test_psf.FindAdaptiveMom() ref_moments = im_ref_psf.FindAdaptiveMom() np.testing.assert_almost_equal( test_moments.moments_sigma, ref_moments.moments_sigma, decimal=pp_decimal-1, err_msg="Inconsistent OpticalPSF image for rotated model after loading pupil plane.") # Now include aberrations. Here we are testing the ability to figure out the pupil plane extent # and sampling appropriately. Those get fed into the routine for making the aberrations. defocus = -0.03 coma1 = 0.03 spher = -0.02 ref_psf = galsim.OpticalPSF(lam_over_diam, obscuration=obscuration, nstruts=nstruts, strut_angle=strut_angle, defocus=defocus, coma1=coma1, spher=spher, oversampling=pp_oversampling, pad_factor=pp_pad_factor) test_psf = galsim.OpticalPSF(lam_over_diam, obscuration=obscuration, pupil_plane_im=im, defocus=defocus, coma1=coma1, spher=spher, oversampling=pp_oversampling, pad_factor=pp_pad_factor) im_ref_psf = ref_psf.drawImage(scale=scale) im_test_psf = galsim.ImageD(im_ref_psf.array.shape[0], im_ref_psf.array.shape[1]) im_test_psf = test_psf.drawImage(image=im_test_psf, scale=scale) if pp_test_type == 'image': np.testing.assert_array_almost_equal( im_test_psf.array, im_ref_psf.array, decimal=pp_decimal, err_msg="Inconsistent OpticalPSF image for aberrated model after loading pupil plane.") else: test_moments = im_test_psf.FindAdaptiveMom() ref_moments = im_ref_psf.FindAdaptiveMom() np.testing.assert_almost_equal( test_moments.moments_sigma, ref_moments.moments_sigma, decimal=pp_decimal, err_msg="Inconsistent OpticalPSF image for aberrated model after loading pupil plane.") # Test for preservation of symmetries: the result should be the same if the pupil plane is # rotated by integer multiples of 2pi/(nstruts). ref_psf = galsim.OpticalPSF(lam_over_diam, obscuration=obscuration, nstruts=nstruts, strut_angle=strut_angle, oversampling=pp_oversampling, pad_factor=pp_pad_factor) im_ref_psf = ref_psf.drawImage(scale=scale) for ind in range(1,nstruts): rot_angle = ind*2.*np.pi/nstruts test_psf = galsim.OpticalPSF(lam_over_diam, obscuration=obscuration, pupil_plane_im=im, pupil_angle=rot_angle*galsim.radians, oversampling=pp_oversampling, pad_factor=pp_pad_factor) im_test_psf = galsim.ImageD(im_ref_psf.array.shape[0], im_ref_psf.array.shape[1]) im_test_psf = test_psf.drawImage(image=im_test_psf, scale=scale) if pp_test_type == 'image': np.testing.assert_array_almost_equal( im_test_psf.array, im_ref_psf.array, decimal=pp_decimal, err_msg="Inconsistent OpticalPSF image after rotating pupil plane by invariant " "angle.") else: test_moments = im_test_psf.FindAdaptiveMom() ref_moments = im_test_psf.FindAdaptiveMom() np.testing.assert_almost_equal( test_moments.moments_sigma, ref_moments.moments_sigma, decimal=pp_decimal, err_msg="Inconsistent OpticalPSF image after rotating pupil plane by invariant " "angle.") # Test that if we rotate pupil plane with no aberrations, that's equivalent to rotating the PSF # itself. Use rotation angle of 90 degrees so numerical issues due to the interpolation should # be minimal. rot_angle = 90.*galsim.degrees psf_1 = galsim.OpticalPSF(lam_over_diam, obscuration=obscuration, pupil_plane_im=im, oversampling=pp_oversampling, pad_factor=pp_pad_factor) rot_psf_1 = psf_1.rotate(rot_angle) psf_2 = galsim.OpticalPSF(lam_over_diam, obscuration=obscuration, pupil_plane_im=im, pupil_angle=rot_angle, oversampling=pp_oversampling, pad_factor=pp_pad_factor) im_1 = psf_1.drawImage(scale=scale) im_2 = galsim.ImageD(im_1.array.shape[0], im_1.array.shape[1]) im_2 = psf_2.drawImage(image=im_2, scale=scale) if pp_test_type == 'image': np.testing.assert_array_almost_equal( im_1.array, im_2.array, decimal=pp_decimal, err_msg="Inconsistent OpticalPSF image after rotating pupil plane vs. rotating PSF.") else: test_moments = im_1.FindAdaptiveMom() ref_moments = im_2.FindAdaptiveMom() np.testing.assert_almost_equal( test_moments.moments_sigma, ref_moments.moments_sigma, decimal=pp_decimal, err_msg="Inconsistent OpticalPSF image after rotating pupil plane vs. rotating PSF.") # Supply the pupil plane at higher resolution, and make sure that the routine figures out the # sampling and gets the right image scale etc. rescale_fac = 0.77 ref_psf = galsim.OpticalPSF(lam_over_diam, obscuration=obscuration, nstruts=nstruts, strut_angle=strut_angle, oversampling=pp_oversampling, pad_factor=pp_pad_factor/rescale_fac) # Make higher resolution pupil plane image via interpolation int_im = galsim.InterpolatedImage(galsim.Image(im, scale=1.0, dtype=np.float32), calculate_maxk=False, calculate_stepk=False, x_interpolant='linear') new_im = int_im.drawImage(scale=rescale_fac, method='no_pixel') new_im.wcs = None # Let OpticalPSF figure out the scale automatically. test_psf = galsim.OpticalPSF(lam_over_diam, obscuration=obscuration, pupil_plane_im=new_im, oversampling=pp_oversampling) im_ref_psf = ref_psf.drawImage(scale=scale) im_test_psf = galsim.ImageD(im_ref_psf.array.shape[0], im_ref_psf.array.shape[1]) im_test_psf = test_psf.drawImage(image=im_test_psf, scale=scale) test_moments = im_test_psf.FindAdaptiveMom() ref_moments = im_ref_psf.FindAdaptiveMom() if pp_test_type == 'image': np.testing.assert_almost_equal( test_moments.moments_sigma/ref_moments.moments_sigma-1., 0, decimal=2, err_msg="Inconsistent OpticalPSF image for basic model after loading high-res pupil plane.") else: np.testing.assert_almost_equal( test_moments.moments_sigma/ref_moments.moments_sigma-1., 0, decimal=1, err_msg="Inconsistent OpticalPSF image for basic model after loading high-res pupil plane.") # Now supply the pupil plane at the original resolution, but remove some of the padding. We # want it to properly recognize that it needs more padding, and include it. remove_pad = -23 sub_im = im[im.bounds.withBorder(remove_pad)] test_psf = galsim.OpticalPSF(lam_over_diam, obscuration=obscuration, pupil_plane_im=sub_im, oversampling=pp_oversampling, pad_factor=pp_pad_factor) im_test_psf = galsim.ImageD(im_ref_psf.array.shape[0], im_ref_psf.array.shape[1]) im_test_psf = test_psf.drawImage(image=im_test_psf, scale=scale) test_moments = im_test_psf.FindAdaptiveMom() ref_moments = im_ref_psf.FindAdaptiveMom() np.testing.assert_almost_equal( test_moments.moments_sigma/ref_moments.moments_sigma-1., 0, decimal=pp_decimal-3, err_msg="Inconsistent OpticalPSF image for basic model after loading less padded pupil plane.") # Now supply the pupil plane at the original resolution, with extra padding. new_pad = 76 big_im = galsim.Image(im.bounds.withBorder(new_pad)) big_im[im.bounds] = im test_psf = galsim.OpticalPSF(lam_over_diam, obscuration=obscuration, pupil_plane_im=big_im, oversampling=pp_oversampling, pad_factor=pp_pad_factor) im_test_psf = galsim.ImageD(im_ref_psf.array.shape[0], im_ref_psf.array.shape[1]) im_test_psf = test_psf.drawImage(image=im_test_psf, scale=scale) test_moments = im_test_psf.FindAdaptiveMom() ref_moments = im_ref_psf.FindAdaptiveMom() np.testing.assert_almost_equal( test_moments.moments_sigma, ref_moments.moments_sigma, decimal=pp_decimal-2, err_msg="Inconsistent OpticalPSF image size for basic model " "after loading more padded pupil plane.") # Check for same answer if we use image, array, or filename for reading in array. test_psf = galsim.OpticalPSF(lam_over_diam, obscuration=obscuration, pupil_plane_im=im, oversampling=pp_oversampling, pad_factor=pp_pad_factor) im_test_psf = test_psf.drawImage(scale=scale) test_psf_2 = galsim.OpticalPSF(lam_over_diam, obscuration=obscuration, pupil_plane_im=im.array, oversampling=pp_oversampling, pad_factor=pp_pad_factor) im_test_psf_2 = test_psf_2.drawImage(scale=scale) np.testing.assert_almost_equal( im_test_psf.array, im_test_psf_2.array, decimal=pp_decimal, err_msg="Inconsistent OpticalPSF image from Image vs. array.") # The following had used lam_over_diam, but that is now invalid because the fits file # has a specific pixel scale. So we need to provide lam and diam separately so that the # units are consistent. diam = 500.e-9 / lam_over_diam * galsim.radians / galsim.arcsec test_psf_3 = galsim.OpticalPSF( lam=500, diam=diam, obscuration=obscuration, oversampling=pp_oversampling, pupil_plane_im=os.path.join(imgdir, pp_file), pad_factor=pp_pad_factor) im_test_psf_3 = test_psf_3.drawImage(scale=scale) np.testing.assert_almost_equal( im_test_psf.array, im_test_psf_3.array, decimal=pp_decimal, err_msg="Inconsistent OpticalPSF image from Image vs. file read-in.")
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 test_ne(): # Use some very forgiving settings to speed up this test. We're not actually going to draw # any images (other than internally the PSF), so should be okay. gsp1 = galsim.GSParams(maxk_threshold=5.e-2, folding_threshold=5e-2, kvalue_accuracy=1e-3, xvalue_accuracy=1e-3) gsp2 = galsim.GSParams(maxk_threshold=5.1e-2, folding_threshold=5e-2, kvalue_accuracy=1e-3, xvalue_accuracy=1e-3) pupil_plane_im = galsim.fits.read(os.path.join(imgdir, pp_file)) pupil_plane_im.wcs = None # Params include: lam_over_diam, (lam/diam), aberrations by name, aberrations by list, nstruts, # strut_thick, strut_angle, obscuration, oversampling, pad_factor, flux, gsparams, # circular_pupil, interpolant, pupil_plane_im, pupil_angle, scale_unit objs = [galsim.OpticalPSF(lam_over_diam=1.0, gsparams=gsp1), galsim.OpticalPSF(lam_over_diam=1.0, gsparams=gsp2), galsim.OpticalPSF(lam=1.0, diam=1.0, gsparams=gsp1), galsim.OpticalPSF(lam=1.0, diam=1.0, scale_unit=galsim.arcmin, gsparams=gsp1), galsim.OpticalPSF(lam_over_diam=1.0, defocus=0.1, gsparams=gsp1), galsim.OpticalPSF(lam_over_diam=1.0, aberrations=[0, 0, 0, 0, 0.2], gsparams=gsp1), galsim.OpticalPSF(lam_over_diam=1.0, nstruts=2, gsparams=gsp1), galsim.OpticalPSF(lam_over_diam=1.0, nstruts=2, strut_thick=0.3, gsparams=gsp1), galsim.OpticalPSF(lam_over_diam=1.0, nstruts=2, strut_angle=10.*galsim.degrees, gsparams=gsp1), galsim.OpticalPSF(lam_over_diam=1.0, obscuration=0.5, gsparams=gsp1), galsim.OpticalPSF(lam_over_diam=1.0, obscuration=0.5, coma1=1.0, gsparams=gsp1), galsim.OpticalPSF(lam_over_diam=1.0, obscuration=0.5, coma1=1.0, annular_zernike=True, gsparams=gsp1), galsim.OpticalPSF(lam_over_diam=1.0, oversampling=2.0, gsparams=gsp1), galsim.OpticalPSF(lam_over_diam=1.0, pad_factor=2.0, gsparams=gsp1), galsim.OpticalPSF(lam_over_diam=1.0, flux=2.0, gsparams=gsp1), galsim.OpticalPSF(lam_over_diam=1.0, circular_pupil=False, gsparams=gsp1), galsim.OpticalPSF(lam_over_diam=1.0, interpolant='Linear', gsparams=gsp1), galsim.OpticalPSF(lam_over_diam=1.0, gsparams=gsp1, ii_pad_factor=2.)] stepk = objs[0].stepk maxk = objs[0].maxk objs += [galsim.OpticalPSF(lam_over_diam=1.0, gsparams=gsp1, _force_stepk=stepk/1.5), galsim.OpticalPSF(lam_over_diam=1.0, gsparams=gsp1, _force_maxk=maxk*2)] if do_slow_tests: objs += [galsim.OpticalPSF(lam_over_diam=1.0, pupil_plane_im=pupil_plane_im, gsparams=gsp1, suppress_warning=True), galsim.OpticalPSF(lam_over_diam=1.0, pupil_plane_im=pupil_plane_im, gsparams=gsp1, pupil_angle=10*galsim.degrees, suppress_warning=True)] all_obj_diff(objs)
import galsim except ImportError: path, filename = os.path.split(__file__) sys.path.append(os.path.abspath(os.path.join(path, "..", ".."))) import galsim if __name__ == "__main__": lod = 0.04 obscuration = 0.3 imsize = 128 # predefine image of fixed size for drawing into im = galsim.ImageD(imsize, imsize) # defocus optics = galsim.OpticalPSF(lod, defocus=.5, obscuration=obscuration) im = optics.draw(im, dx=0.2 * lod) im.write( os.path.join(os.path.abspath(os.path.dirname(__file__)), "optics_defocus.fits")) # astig1 optics = galsim.OpticalPSF(lod, defocus=.5, astig1=.5, obscuration=obscuration) im = optics.draw(im, dx=0.2 * lod) im.write( os.path.join(os.path.abspath(os.path.dirname(__file__)), "optics_astig1.fits"))
def test_geometric_shoot(): """Test that geometric photon shooting is reasonably consistent with Fourier optics.""" jmax = 20 bd = galsim.BaseDeviate(1111111) u = galsim.UniformDeviate(bd) lam = 500.0 diam = 4.0 for i in range(4): # Do a few random tests. Takes about 1 sec. aberrations = [0]+[u()*0.1 for i in range(jmax)] opt_psf = galsim.OpticalPSF(diam=diam, lam=lam, aberrations=aberrations, geometric_shooting=True) # Use really good seeing, so that the optics contribution actually matters. atm_psf = galsim.Kolmogorov(fwhm=0.4) psf = galsim.Convolve(opt_psf, atm_psf) u1 = u.duplicate() im_shoot = psf.drawImage(nx=256, ny=256, scale=0.2, method='phot', n_photons=100000, rng=u) im_fft = psf.drawImage(nx=256, ny=256, scale=0.2) printval(im_fft, im_shoot) shoot_moments = galsim.hsm.FindAdaptiveMom(im_shoot) fft_moments = galsim.hsm.FindAdaptiveMom(im_fft) # 40th of a pixel centroid tolerance. np.testing.assert_allclose( shoot_moments.moments_centroid.x, fft_moments.moments_centroid.x, rtol=0, atol=0.025, err_msg="") np.testing.assert_allclose( shoot_moments.moments_centroid.y, fft_moments.moments_centroid.y, rtol=0, atol=0.025, err_msg="") # 2% size tolerance np.testing.assert_allclose( shoot_moments.moments_sigma, fft_moments.moments_sigma, rtol=0.02, atol=0, err_msg="") # Not amazing ellipticity consistency at the moment. 0.01 tolerance. print(fft_moments.observed_shape) print(shoot_moments.observed_shape) np.testing.assert_allclose( shoot_moments.observed_shape.g1, fft_moments.observed_shape.g1, rtol=0, atol=0.01, err_msg="") np.testing.assert_allclose( shoot_moments.observed_shape.g2, fft_moments.observed_shape.g2, rtol=0, atol=0.01, err_msg="") # Check the flux # The Airy part sends a lot of flux off the edge, so this test is a little loose. added_flux = im_shoot.added_flux print('psf.flux = ',psf.flux) print('added_flux = ',added_flux) print('image flux = ',im_shoot.array.sum()) assert np.isclose(added_flux, psf.flux, rtol=3.e-4) assert np.isclose(im_shoot.array.sum(), psf.flux, rtol=3.e-4) # Check doing this with photon_ops im_shoot2 = opt_psf.drawImage(nx=256, ny=256, scale=0.2, method='phot', n_photons=100000, rng=u1.duplicate(), photon_ops=[atm_psf]) np.testing.assert_allclose(im_shoot2.array, im_shoot.array) im_shoot3 = galsim.DeltaFunction().drawImage(nx=256, ny=256, scale=0.2, method='phot', n_photons=100000, rng=u1.duplicate(), photon_ops=[opt_psf, atm_psf]) np.testing.assert_allclose(im_shoot3.array, im_shoot.array)