def test_crg_roundtrip(): """Test that drawing a ChromaticRealGalaxy using the HST collecting area and filter gives back the original image. """ f606w_cat = galsim.RealGalaxyCatalog('AEGIS_F606w_catalog.fits', dir=image_dir) f814w_cat = galsim.RealGalaxyCatalog('AEGIS_F814w_catalog.fits', dir=image_dir) indices = [0] if __name__ != "__main__" else list(range(len(f606w_cat))) for index in indices: orig_f606w = f606w_cat.getGalImage(index) orig_f814w = f814w_cat.getGalImage(index) crg = galsim.ChromaticRealGalaxy([f606w_cat, f814w_cat], index=index) # Note that getPSF() return value already includes convolution by the pixel f606w_obj = galsim.Convolve(crg, f606w_cat.getPSF(index)) f814w_obj = galsim.Convolve(crg, f814w_cat.getPSF(index)) f606w = f606w_cat.getBandpass() f814w = f814w_cat.getBandpass() im_f606w = f606w_obj.drawImage(f606w, image=orig_f606w.copy(), method='no_pixel') im_f814w = f814w_obj.drawImage(f814w, image=orig_f814w.copy(), method='no_pixel') printval(im_f606w, orig_f606w) printval(im_f814w, orig_f814w) orig_f606w_mom = galsim.hsm.FindAdaptiveMom(orig_f606w) orig_f814w_mom = galsim.hsm.FindAdaptiveMom(orig_f814w) im_f606w_mom = galsim.hsm.FindAdaptiveMom(im_f606w) im_f814w_mom = galsim.hsm.FindAdaptiveMom(im_f814w) # Images are only pixel-by-pixel consistent to 5% or so. However, if you actually plot the # residuals (which you can do by flipping False->True in printval in galsim_test_helpers), # they appear as ringing over the whole image. Probably it's unrealistic to expect this # test to work perfectly since we're effectively deconvolving and then reconvolving by the # same PSF, not a fatter PSF. np.testing.assert_allclose(orig_f606w.array, im_f606w.array, rtol=0., atol=5e-2 * orig_f606w.array.max()) np.testing.assert_allclose(orig_f814w.array, im_f814w.array, rtol=0., atol=5e-2 * orig_f814w.array.max()) # Happily, the pixel-by-pixel residuals don't appear to affect the moments much: np.testing.assert_allclose(orig_f606w_mom.moments_amp, im_f606w_mom.moments_amp, rtol=1e-3, atol=0) np.testing.assert_allclose(orig_f606w_mom.moments_centroid.x, im_f606w_mom.moments_centroid.x, rtol=0., atol=1e-2) np.testing.assert_allclose(orig_f606w_mom.moments_centroid.y, im_f606w_mom.moments_centroid.y, rtol=0., atol=1e-2) np.testing.assert_allclose(orig_f606w_mom.moments_sigma, im_f606w_mom.moments_sigma, rtol=1e-3, atol=0) np.testing.assert_allclose(orig_f606w_mom.observed_shape.g1, im_f606w_mom.observed_shape.g1, rtol=0, atol=2e-4) np.testing.assert_allclose(orig_f606w_mom.observed_shape.g2, im_f606w_mom.observed_shape.g2, rtol=0, atol=2e-4) np.testing.assert_allclose(orig_f814w_mom.moments_amp, im_f814w_mom.moments_amp, rtol=1e-3, atol=0) np.testing.assert_allclose(orig_f814w_mom.moments_centroid.x, im_f814w_mom.moments_centroid.x, rtol=0., atol=1e-2) np.testing.assert_allclose(orig_f814w_mom.moments_centroid.y, im_f814w_mom.moments_centroid.y, rtol=0., atol=1e-2) np.testing.assert_allclose(orig_f814w_mom.moments_sigma, im_f814w_mom.moments_sigma, rtol=1e-3, atol=0) np.testing.assert_allclose(orig_f814w_mom.observed_shape.g1, im_f814w_mom.observed_shape.g1, rtol=0, atol=1e-4) np.testing.assert_allclose(orig_f814w_mom.observed_shape.g2, im_f814w_mom.observed_shape.g2, rtol=0, atol=1e-4)
def __init__(self, real_galaxy_catalog, index=None, id=None, random=False, rng=None, x_interpolant=None, k_interpolant=None, flux=None, flux_rescale=None, pad_factor=4, noise_pad_size=0, gsparams=None, logger=None): import numpy as np if rng is None: self.rng = galsim.BaseDeviate() elif not isinstance(rng, galsim.BaseDeviate): raise TypeError( "The rng provided to RealGalaxy constructor is not a BaseDeviate" ) else: self.rng = rng self._rng = self.rng.duplicate( ) # This is only needed if we want to make sure eval(repr) # results in the same object. if flux is not None and flux_rescale is not None: raise TypeError( "Cannot supply a flux and a flux rescaling factor!") if isinstance(real_galaxy_catalog, tuple): # Special (undocumented) way to build a RealGalaxy without needing the rgc directly # by providing the things we need from it. Used by COSMOSGalaxy. self.gal_image, self.psf_image, noise_image, pixel_scale, var = real_galaxy_catalog use_index = 0 # For the logger statements below. if logger: logger.debug('RealGalaxy %d: Start RealGalaxy constructor.', use_index) self.catalog_file = None else: # Get the index to use in the catalog if index is not None: if id is not None or random is True: raise AttributeError( 'Too many methods for selecting a galaxy!') use_index = index elif id is not None: if random is True: raise AttributeError( 'Too many methods for selecting a galaxy!') use_index = real_galaxy_catalog.getIndexForID(id) elif random is True: uniform_deviate = galsim.UniformDeviate(self.rng) use_index = int(real_galaxy_catalog.nobjects * uniform_deviate()) else: raise AttributeError( 'No method specified for selecting a galaxy!') if logger: logger.debug('RealGalaxy %d: Start RealGalaxy constructor.', use_index) # Read in the galaxy, PSF images; for now, rely on pyfits to make I/O errors. self.gal_image = real_galaxy_catalog.getGal(use_index) if logger: logger.debug('RealGalaxy %d: Got gal_image', use_index) self.psf_image = real_galaxy_catalog.getPSF(use_index) if logger: logger.debug('RealGalaxy %d: Got psf_image', use_index) #self.noise = real_galaxy_catalog.getNoise(use_index, self.rng, gsparams) # We need to duplication some of the RealGalaxyCatalog.getNoise() function, since we # want it to be possible to have the RealGalaxyCatalog in another process, and the # BaseCorrelatedNoise object is not picklable. So we just build it here instead. noise_image, pixel_scale, var = real_galaxy_catalog.getNoiseProperties( use_index) if logger: logger.debug('RealGalaxy %d: Got noise_image', use_index) self.catalog_file = real_galaxy_catalog.getFileName() if noise_image is None: self.noise = galsim.UncorrelatedNoise(var, rng=self.rng, scale=pixel_scale, gsparams=gsparams) else: ii = galsim.InterpolatedImage(noise_image, normalization="sb", calculate_stepk=False, calculate_maxk=False, x_interpolant='linear', gsparams=gsparams) self.noise = galsim.correlatednoise._BaseCorrelatedNoise( self.rng, ii, noise_image.wcs) self.noise = self.noise.withVariance(var) if logger: logger.debug('RealGalaxy %d: Finished building noise', use_index) # Save any other relevant information as instance attributes self.catalog = real_galaxy_catalog self.index = use_index self.pixel_scale = float(pixel_scale) self._x_interpolant = x_interpolant self._k_interpolant = k_interpolant self._pad_factor = pad_factor self._noise_pad_size = noise_pad_size self._flux = flux self._gsparams = gsparams # Convert noise_pad to the right noise to pass to InterpolatedImage if noise_pad_size: noise_pad = self.noise else: noise_pad = 0. # Build the InterpolatedImage of the PSF. self.original_psf = galsim.InterpolatedImage( self.psf_image, x_interpolant=x_interpolant, k_interpolant=k_interpolant, flux=1.0, gsparams=gsparams) if logger: logger.debug('RealGalaxy %d: Made original_psf', use_index) # Build the InterpolatedImage of the galaxy. # Use the stepK() value of the PSF as a maximum value for stepK of the galaxy. # (Otherwise, low surface brightness galaxies can get a spuriously high stepk, which # leads to problems.) self.original_gal = galsim.InterpolatedImage( self.gal_image, x_interpolant=x_interpolant, k_interpolant=k_interpolant, pad_factor=pad_factor, noise_pad_size=noise_pad_size, calculate_stepk=self.original_psf.stepK(), calculate_maxk=self.original_psf.maxK(), noise_pad=noise_pad, rng=self.rng, gsparams=gsparams) if logger: logger.debug('RealGalaxy %d: Made original_gal', use_index) # If flux is None, leave flux as given by original image if flux is not None: flux_rescale = flux / self.original_gal.getFlux() if flux_rescale is not None: self.original_gal *= flux_rescale self.noise *= flux_rescale**2 # Calculate the PSF "deconvolution" kernel psf_inv = galsim.Deconvolve(self.original_psf, gsparams=gsparams) # Initialize the SBProfile attribute GSObject.__init__( self, galsim.Convolve([self.original_gal, psf_inv], gsparams=gsparams)) if logger: logger.debug('RealGalaxy %d: Made gsobject', use_index) # Save the noise in the image as an accessible attribute self.noise = self.noise.convolvedWith(psf_inv, gsparams) if logger: logger.debug('RealGalaxy %d: Finished building RealGalaxy', use_index)
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.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.true_center # 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 21 arcsec, # which is large enough to cover all the way to the corners, although we'll need # to watch out for galaxies that are fully off the edge of the image. radius = 21 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 (available as stamp.center) # cannot be at the true center (available as stamp.true_center) 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([gal, psf]) # 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 # If there is no overlap, then the intersection comes out as not defined, which we # can check with bounds.isDefined(). if not bounds.isDefined(): logger.info("object %d is fully off the edge of the image. Skipping this one.", k) continue 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
bulge_frac = frac bulge = galsim.DeVaucouleurs(half_light_radius=dev_radius * scale) bulge = bulge.shear(e1=dev_e2, e2=dev_e2) flux = flux_pl.sample() gal = frac * bulge + (1 - frac) * disk redshift = np.random.rand() * max_redshift sed = np.random.choice(template_seds, 1)[0].atRedshift(redshift) #normalize flux according to specified filter sed = sed.withFlux(flux, filters[args.filter_norm]) mgal = galsim.Chromatic(gal, sed) cgal = galsim.Convolve([mgal, psf]) gtrue.rad = frac * dev_radius + (1 - frac) * exp_radius gtrue.sed = sed gtrue.redshift = redshift true_x.append(gtrue.x) true_y.append(gtrue.y) for filter_name, filter_ in filters.items(): tmp = cgal.drawImage(filter_, image=images[filter_name], add_to_image=True, offset=(offsetx, offsety)) gtrue.fluxes[filter_name] = tmp.added_flux
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(flux=0.4, half_light_radius=0.7, n=3.2, trunc=8.5, gsparams=gsparams) disk = galsim.Sersic(flux=0.6, half_light_radius=1.2, n=1.5, gsparams=gsparams) gal5 = bulge + 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 + 1) # 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.flux) 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 + 1) 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.e1, gal_shape.e2) 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)
# # this script was used to generate the idealized Gaussian test cases used in ../test_real.py import galsim fake_gal_fwhm = 0.7 # arcsec fake_gal_shear1 = 0.29 # shear representing intrinsic shape component 1 fake_gal_shear2 = -0.21 # shear representing intrinsic shape component 2; note non-round, to detect # possible issues with x<->y or others that might not show up using circular galaxy fake_gal_flux = 1000.0 fake_gal_orig_PSF_fwhm = 0.1 # arcsec fake_gal_orig_PSF_shear1 = 0.0 fake_gal_orig_PSF_shear2 = -0.07 pixel_scale = 0.03 # high-resolution data orig_gal = galsim.Gaussian(flux = fake_gal_flux, fwhm = fake_gal_fwhm) orig_gal.applyShear(fake_gal_shear1, fake_gal_shear2) orig_PSF = galsim.Gaussian(flux = 1.0, fwhm = fake_gal_orig_PSF_fwhm) orig_PSF.applyShear(fake_gal_orig_PSF_shear1, fake_gal_orig_PSF_shear2) orig_observed = galsim.Convolve(orig_gal, orig_PSF) obs_image = galsim.ImageF(200, 200) orig_observed.draw(obs_image, dx = pixel_scale) obs_PSF_image = galsim.ImageF(30, 30) orig_PSF.draw(obs_PSF_image, dx = pixel_scale) obs_image.write('tmp_obs_image.fits', clobber=True) obs_PSF_image.write('tmp_obs_PSF_image.fits', clobber = True)
def _run_metacal(*, n_sims, rng, swap_g1g2, dudx, dudy, dvdx, dvdy): """Run metacal on an image composed of stamps w/ constant noise. Parameters ---------- n_sims : int The number of objects to run. rng : np.random.RandomState An RNG to use. swap_g1g2 : bool If True, set the true shear on the 2-axis to 0.02 and 1-axis to 0.0. Otherwise, the true shear on the 1-axis is 0.02 and on the 2-axis is 0.0. dudx : float The du/dx Jacobian component. dudy : float The du/dy Jacobian component. dydx : float The dv/dx Jacobian component. dvdy : float The dv/dy Jacobian component. Returns ------- result : dict A dictionary with each of the metacal catalogs. """ method = 'auto' stamp_size = 33 psf_stamp_size = 33 cen = (stamp_size - 1) / 2 psf_cen = (psf_stamp_size - 1) / 2 s2n = 1e16 flux = 1e6 galsim_jac = galsim.JacobianWCS(dudx=dudx, dudy=dudy, dvdx=dvdx, dvdy=dvdy) if swap_g1g2: g1 = 0.0 g2 = 0.02 else: g1 = 0.02 g2 = 0.0 gal = galsim.Exponential(half_light_radius=0.5).withFlux(flux).shear(g1=g1, g2=g2) psf = galsim.Gaussian(fwhm=0.9).withFlux(1) obj = galsim.Convolve(gal, psf) obj_im = obj.drawImage(nx=111, ny=111).array noise = np.sqrt(np.sum(obj_im**2)) / s2n data = [] for ind in tqdm.trange(n_sims): ################################ # make the obs # psf psf_im = psf.drawImage(nx=psf_stamp_size, ny=psf_stamp_size, wcs=galsim_jac, method=method).array psf_noise = np.sqrt(np.sum(psf_im**2)) / 10000 wgt = np.ones_like(psf_im) / psf_noise**2 psf_im += (rng.normal(size=psf_im.shape) * psf_noise) psf_jac = ngmix.Jacobian(x=psf_cen, y=psf_cen, dudx=dudx, dudy=dudy, dvdx=dvdx, dvdy=dvdy) psf_obs = ngmix.Observation(image=psf_im, weight=wgt, jacobian=psf_jac) # now render object scale = psf_jac.scale shift = rng.uniform(low=-scale / 2, high=scale / 2, size=2) _obj = obj.shift(dx=shift[0], dy=shift[1]) xy = galsim_jac.toImage(galsim.PositionD(shift)) im = _obj.drawImage(nx=stamp_size, ny=stamp_size, wcs=galsim_jac, method=method).array jac = ngmix.Jacobian(x=cen + xy.x, y=cen + xy.y, dudx=dudx, dudy=dudy, dvdx=dvdx, dvdy=dvdy) wgt = np.ones_like(im) / noise**2 nse = rng.normal(size=im.shape) * noise im += (rng.normal(size=im.shape) * noise) obs = ngmix.Observation(image=im, weight=wgt, noise=nse, bmask=np.zeros_like(im, dtype=np.int32), ormask=np.zeros_like(im, dtype=np.int32), jacobian=jac, psf=psf_obs) # build the mbobs mbobs = ngmix.MultiBandObsList() obslist = ngmix.ObsList() obslist.append(obs) mbobs.append(obslist) mbobs.meta['id'] = ind + 1 # these settings do not matter that much I think mbobs[0].meta['Tsky'] = 1 mbobs[0].meta['magzp_ref'] = 26.5 mbobs[0][0].meta['orig_col'] = ind + 1 mbobs[0][0].meta['orig_row'] = ind + 1 ################################ # run the fitters try: res = _run_metacal_fitter(mbobs, rng) except Exception as e: print(e) res = None if res is not None: data.append(res) if len(data) > 0: res = data else: res = None return res
def main(argv): """ Make images using constant PSF and variable shear: - The main image is 2048 x 2048 pixels. - Pixel scale is 0.2 arcsec/pixel, hence the image is about 0.11 degrees on a side. - Applied shear is from a cosmological power spectrum read in from file. - The PSF is a real one from SDSS, and corresponds to a convolution of atmospheric PSF, optical PSF, and pixel response, which has been sampled at pixel centers. We used a PSF from SDSS in order to have a PSF profile that could correspond to what you see with a real telescope. However, in order that the galaxy resolution not be too poor, we tell GalSim that the pixel scale for that PSF image is 0.2" rather than 0.396". We are simultaneously lying about the intrinsic size of the PSF and about the pixel scale when we do this. - The galaxies come from COSMOSCatalog, which can produce either RealGalaxy profiles (like in demo10) and parametric fits to those profiles. We choose 30% of the galaxies to use the images, and the other 60% to use the parametric fits - The real galaxy images include some initial correlated noise from the original HST observation. However, we whiten the noise of the final image so the final image has stationary Gaussian noise, rather than correlated noise. """ logging.basicConfig(format="%(message)s", level=logging.INFO, stream=sys.stdout) logger = logging.getLogger("demo11") # Define some parameters we'll use below. # Normally these would be read in from some parameter file. pixel_scale = 0.2 # arcsec/pixel image_size = 2048 # size of image in pixels image_size_arcsec = image_size * pixel_scale # size of big image in each dimension (arcsec) noise_variance = 5.e4 # ADU^2 (Just use simple Gaussian noise here.) nobj = 288 # number of galaxies in entire field # (This corresponds to 8 galaxies / arcmin^2) grid_spacing = 90.0 # The spacing between the samples for the power spectrum # realization (arcsec) tel_diam = 4 # Let's figure out the flux for a 4 m class telescope exp_time = 300 # exposing for 300 seconds. center_ra = 19.3 * galsim.hours # The RA, Dec of the center of the image on the sky center_dec = -33.1 * galsim.degrees # The catalog returns objects that are appropriate for HST in 1 second exposures. So for our # telescope we scale up by the relative area and exposure time. Note that what is important is # the *effective* area after taking into account obscuration. For HST, the telescope diameter # is 2.4 but there is obscuration (a linear factor of 0.33). Here, we assume that the telescope # we're simulating effectively has no obscuration factor. We're also ignoring the pi/4 factor # since it appears in the numerator and denominator, so we use area = diam^2. hst_eff_area = 2.4**2 * (1. - 0.33**2) flux_scaling = (tel_diam**2 / hst_eff_area) * exp_time # random_seed is used for both the power spectrum realization and the random properties # of the galaxies. random_seed = 24783923 file_name = os.path.join('output', 'tabulated_power_spectrum.fits.fz') logger.info('Starting demo script 11') # Read in galaxy catalog # The COSMOSCatalog uses the same input file as we have been using for RealGalaxyCatalogs # along with a second file called real_galaxy_catalog_23.5_examples_fits.fits, which stores # the information about the parameteric fits. There is no need to specify the second file # name, since the name is derivable from the name of the main catalog. if True: # The catalog we distribute with the GalSim code only has 100 galaxies. # The galaxies will typically be reused several times here. cat_file_name = 'real_galaxy_catalog_23.5_example.fits' dir = 'data' cosmos_cat = galsim.COSMOSCatalog(cat_file_name, dir=dir) else: # If you've run galsim_download_cosmos, you can leave out the cat_file_name and dir # to use the full COSMOS catalog with 56,000 galaxies in it. cosmos_cat = galsim.COSMOSCatalog() logger.info('Read in %d galaxies from catalog', cosmos_cat.nobjects) # Setup the PowerSpectrum object we'll be using: # To do this, we first have to read in the tabulated shear power spectrum, often denoted # C_ell(ell), where ell has units of inverse angle and C_ell has units of angle^2. However, # GalSim works in the flat-sky approximation, so we use this notation interchangeably with # P(k). GalSim does not calculate shear power spectra for users, who must be able to provide # their own (or use the examples in the repository). # # Here we use a tabulated power spectrum from iCosmo (http://icosmo.org), with the following # cosmological parameters and survey design: # H_0 = 70 km/s/Mpc # Omega_m = 0.25 # Omega_Lambda = 0.75 # w_0 = -1.0 # w_a = 0.0 # n_s = 0.96 # sigma_8 = 0.8 # Smith et al. prescription for the non-linear power spectrum. # Eisenstein & Hu transfer function with wiggles. # Default dN/dz with z_med = 1.0 # The file has, as required, just two columns which are k and P(k). However, iCosmo works in # terms of ell and C_ell; ell is inverse radians and C_ell in radians^2. Since GalSim tends to # work in terms of arcsec, we have to tell it that the inputs are radians^-1 so it can convert # to store in terms of arcsec^-1. pk_file = os.path.join('data', 'cosmo-fid.zmed1.00.out') ps = galsim.PowerSpectrum(pk_file, units=galsim.radians) # The argument here is "e_power_function" which defines the E-mode power to use. logger.info('Set up power spectrum from tabulated P(k)') # Now let's read in the PSF. It's a real SDSS PSF, which means pixel scale of 0.396". However, # the typical seeing is 1.2" and we want to simulate better seeing, so we will just tell GalSim # that the pixel scale is 0.2". We have to be careful with SDSS PSF images, as they have an # added 'soft bias' of 1000 which has been removed before creation of this file, so that the sky # level is properly zero. Also, the file is bzipped, to demonstrate the ability of GalSim # handle this kind of compressed file (among others). We read the image directly into an # InterpolatedImage GSObject, so we can manipulate it as needed (here, the only manipulation # needed is convolution). The flux is 1 as needed for a PSF. psf_file = os.path.join('data', 'example_sdss_psf_sky0.fits.bz2') psf = galsim.InterpolatedImage(psf_file, scale=pixel_scale, flux=1.) logger.info('Read in PSF image from bzipped FITS file') # Setup the image: full_image = galsim.ImageF(image_size, image_size) # The default convention for indexing an image is to follow the FITS standard where the # lower-left pixel is called (1,1). However, this can be counter-intuitive to people more # used to C or python indexing, where indices start at 0. It is possible to change the # coordinates of the lower-left pixel with the methods `setOrigin`. For this demo, we # switch to 0-based indexing, so the lower-left pixel will be called (0,0). full_image.setOrigin(0, 0) # As for demo10, we use random_seed for the random numbers required for the # whole image. In this case, both the power spectrum realization and the noise on the # full image we apply later. rng = galsim.BaseDeviate(random_seed) # We want to make random positions within our image. However, currently for shears from a power # spectrum we first have to get shears on a grid of positions, and then we can choose random # positions within that. So, let's make the grid. We're going to make it as large as the # image, with grid points spaced by 90 arcsec (hence interpolation only happens below 90" # scales, below the interesting scales on which we want the shear power spectrum to be # represented exactly). The lensing engine wants positions in arcsec, so calculate that: ps.buildGrid(grid_spacing=grid_spacing, ngrid=int(math.ceil(image_size_arcsec / grid_spacing)), rng=rng) logger.info('Made gridded shears') # We keep track of how much noise is already in the image from the RealGalaxies. # The default initial value is all pixels = 0. noise_image = galsim.ImageF(image_size, image_size) noise_image.setOrigin(0, 0) # Make a slightly non-trivial WCS. We'll use a slightly rotated coordinate system # and center it at the image center. theta = 0.17 * galsim.degrees # ( dudx dudy ) = ( cos(theta) -sin(theta) ) * pixel_scale # ( dvdx dvdy ) ( sin(theta) cos(theta) ) # Note: if you use numpy trig rather than math trig functions, you can call them directly # with a galsim.Angle instance (e.g. theta here) and it will work correctly. dudx = numpy.cos(theta) * pixel_scale dudy = -numpy.sin(theta) * pixel_scale dvdx = numpy.sin(theta) * pixel_scale dvdy = numpy.cos(theta) * pixel_scale image_center = full_image.true_center affine = galsim.AffineTransform(dudx, dudy, dvdx, dvdy, origin=full_image.true_center) # We can also put it on the celestial sphere to give it a bit more realism. # The TAN projection takes a (u,v) coordinate system on a tangent plane and projects # that plane onto the sky using a given point as the tangent point. The tangent # point should be given as a CelestialCoord. sky_center = galsim.CelestialCoord(ra=center_ra, dec=center_dec) # The third parameter, units, defaults to arcsec, but we make it explicit here. # It sets the angular units of the (u,v) intermediate coordinate system. wcs = galsim.TanWCS(affine, sky_center, units=galsim.arcsec) full_image.wcs = wcs # Now we need to loop over our objects: for k in range(nobj): time1 = time.time() # The usual random number generator using a different seed for each galaxy. ud = galsim.UniformDeviate(random_seed + k + 1) # Choose a random RA, Dec around the sky_center. # Note that for this to come out close to a square shape, we need to account for the # cos(dec) part of the metric: ds^2 = dr^2 + r^2 d(dec)^2 + r^2 cos^2(dec) d(ra)^2 # So need to calculate dec first. dec = center_dec + (ud() - 0.5) * image_size_arcsec * galsim.arcsec ra = center_ra + ( ud() - 0.5) * image_size_arcsec / numpy.cos(dec) * galsim.arcsec world_pos = galsim.CelestialCoord(ra, dec) # We will need the image position as well, so use the wcs to get that image_pos = wcs.toImage(world_pos) # We also need this in the tangent plane, which we call "world coordinates" here, # since the PowerSpectrum class is really defined on that plane, not in (ra,dec). uv_pos = affine.toWorld(image_pos) # Get the reduced shears and magnification at this point g1, g2, mu = ps.getLensing(pos=uv_pos) # Now we will have the COSMOSCatalog make a galaxy profile for us. It can make either # a RealGalaxy using the original HST image and PSF, or a parametric model based on # parametric fits to the light distribution of the HST observation. The parametric # models are either a Sersic fit to the data or a bulge + disk fit according to which # one gave the better chisq value. We will select a galaxy at random from the catalog. # One could easily do this by choosing an index = int(ud() * cosmos_cat.nobjects), but # we will instead allow the catalog to choose a random galaxy for us. It will remove any # selection effects involved in postage stamp creation using weights that are stored in # the catalog. (If for some reason you prefer not to do that, you can always choose a # purely random index yourself using int(ud() * cosmos_cat.nobjects).) We employ this # random selection by simply failing to specify an index or identifier for a galaxy, in # which case it chooses a random one. # First determine whether we will make a real galaxy (`gal_type = 'real'`) or a parametric # galaxy (`gal_type = 'parametric'`). The real galaxies take longer to render, so for this # script, we just use them 30% of the time and use parametric galaxies the other 70%. # We could just use `ud()<0.3` for this, but instead we introduce another Deviate type # available in GalSim that we haven't used yet: BinomialDeviate. # It takes an N and p value and returns integers according to a binomial distribution. # i.e. How many heads you get after N flips if each flip has a chance, p, of being heads. binom = galsim.BinomialDeviate(ud, N=1, p=0.3) real = binom() if real: # For real galaxies, we will want to whiten the noise in the image (below). # When whitening the image, we need to make sure the original correlated noise is # present throughout the whole image, otherwise the whitening will do the wrong thing # to the parts of the image that don't include the original image. The RealGalaxy # stores the correct noise profile to use as the gal.noise attribute. This noise # profile is automatically updated as we shear, dilate, convolve, etc. But we need to # tell it how large to pad with this noise by hand. This is a bit complicated for the # code to figure out on its own, so we have to supply the size for noise padding # with the noise_pad_size parameter. # The large galaxies will render fine without any noise padding, but the postage stamp # for the smaller galaxies will be sized appropriately for the PSF, which may make the # stamp larger than the original galaxy image. The psf image is 40 x 40, although # the bright part is much more concentrated than that. If we pad out the galaxy image # to at least 40 x sqrt(2), we should be safe even if the galaxy image is rotated # with respect to the psf image. # noise_pad_size = 40 * sqrt(2) * 0.2 arcsec/pixel = 11.3 arcsec gal = cosmos_cat.makeGalaxy(gal_type='real', rng=ud, noise_pad_size=11.3) else: gal = cosmos_cat.makeGalaxy(gal_type='parametric', rng=ud) # Apply a random rotation theta = ud() * 2.0 * numpy.pi * galsim.radians gal = gal.rotate(theta) # Rescale the flux to match our telescope configuration. # This automatically scales up the noise variance by flux_scaling**2. gal *= flux_scaling # Apply the cosmological (reduced) shear and magnification at this position using a single # GSObject method. gal = gal.lens(g1, g2, mu) # Convolve with the PSF. final = galsim.Convolve(psf, gal) # Account for the fractional part of the position # cf. demo9.py for an explanation of this nominal position stuff. x_nominal = image_pos.x + 0.5 y_nominal = image_pos.y + 0.5 ix_nominal = int(math.floor(x_nominal + 0.5)) iy_nominal = int(math.floor(y_nominal + 0.5)) dx = x_nominal - ix_nominal dy = y_nominal - iy_nominal offset = galsim.PositionD(dx, dy) # We use method='no_pixel' here because the SDSS PSF image that we are using includes the # pixel response already. stamp = final.drawImage(wcs=wcs.local(image_pos), offset=offset, method='no_pixel') # Recenter the stamp at the desired position: stamp.setCenter(ix_nominal, iy_nominal) # Find the overlapping bounds: bounds = stamp.bounds & full_image.bounds # Now, if we are using a real galaxy, we want to ether whiten or at least symmetrize the # noise on the postage stamp to avoid having to deal with correlated noise in any kind of # image processing you would want to do on the final image. (Like measure galaxy shapes.) # Galsim automatically propagates the noise correctly from the initial RealGalaxy object # through the applied shear, distortion, rotation, and convolution into the final object's # noise attribute. To make the noise fully white, use the image.whitenNoise() method. # The returned value is the variance of the Gaussian noise that is present after the # whitening process. # However, this is often overkill for many applications. If it is acceptable to merely end # up with noise with some degree of symmetry (say 4-fold or 8-fold symmetry), then you can # instead have GalSim just add enough noise to make the resulting noise have this kind of # symmetry. Usually this requires adding significantly less additional noise, which means # you can have the resulting total variance be somewhat smaller. The returned variance # corresponds to the zero-lag value of the noise correlation function, which will still have # off-diagonal elements. We can do this step using the image.symmetrizeNoise() method. if real: if True: # We use the symmetrizing option here. new_variance = stamp.symmetrizeNoise(final.noise, 8) else: # Here is how you would do it if you wanted to fully whiten the image. new_variance = stamp.whitenNoise(final.noise) # We need to keep track of how much variance we have currently in the image, so when # we add more noise, we can omit what is already there. noise_image[bounds] += new_variance # Finally, add the stamp to the full image. full_image[bounds] += stamp[bounds] time2 = time.time() tot_time = time2 - time1 logger.info('Galaxy %d: position relative to center = %s, t=%f s', k, str(uv_pos), tot_time) # We already have some noise in the image, but it isn't uniform. So the first thing to do is # to make the Gaussian noise uniform across the whole image. We have a special noise class # that can do this. VariableGaussianNoise takes an image of variance values and applies # Gaussian noise with the corresponding variance to each pixel. # So all we need to do is build an image with how much noise to add to each pixel to get us # up to the maximum value that we already have in the image. max_current_variance = numpy.max(noise_image.array) noise_image = max_current_variance - noise_image vn = galsim.VariableGaussianNoise(rng, noise_image) full_image.addNoise(vn) # Now max_current_variance is the noise level across the full image. We don't want to add that # twice, so subtract off this much from the intended noise that we want to end up in the image. noise_variance -= max_current_variance # Now add Gaussian noise with this variance to the final image. We have to do this step # at the end, rather than adding to individual postage stamps, in order to get the noise # level right in the overlap regions between postage stamps. noise = galsim.GaussianNoise(rng, sigma=math.sqrt(noise_variance)) full_image.addNoise(noise) logger.info('Added noise to final large image') # Now write the image to disk. It is automatically compressed with Rice compression, # since the filename we provide ends in .fz. full_image.write(file_name) logger.info('Wrote image to %r', file_name) # Compute some sky positions of some of the pixels to compare with the values of RA, Dec # that ds9 reports. ds9 always uses (1,1) for the lower left pixel, so the pixel coordinates # of these pixels are different by 1, but you can check that the RA and Dec values are # the same as what GalSim calculates. ra_str = center_ra.hms() dec_str = center_dec.dms() logger.info('Center of image is at RA %sh %sm %ss, DEC %sd %sm %ss', ra_str[0:3], ra_str[3:5], ra_str[5:], dec_str[0:3], dec_str[3:5], dec_str[5:]) for (x, y) in [(0, 0), (0, image_size - 1), (image_size - 1, 0), (image_size - 1, image_size - 1)]: world_pos = wcs.toWorld(galsim.PositionD(x, y)) ra_str = world_pos.ra.hms() dec_str = world_pos.dec.dms() logger.info('Pixel (%4d, %4d) is at RA %sh %sm %ss, DEC %sd %sm %ss', x, y, ra_str[0:3], ra_str[3:5], ra_str[5:], dec_str[0:3], dec_str[3:5], dec_str[5:]) logger.info( 'ds9 reports these pixels as (1,1), (1,2048), etc. with the same RA, Dec.' )
def main(argv): """ A little bit more sophisticated, but still pretty basic: - Use a sheared, exponential profile for the galaxy. - Convolve it by a circular Moffat PSF. - Add Poisson noise to the image. """ # In non-script code, use getLogger(__name__) at module scope instead. logging.basicConfig(format="%(message)s", level=logging.INFO, stream=sys.stdout) logger = logging.getLogger("demo2") gal_flux = 1.e5 # counts gal_r0 = 2.7 # arcsec g1 = 0.1 # g2 = 0.2 # psf_beta = 5 # psf_re = 1.0 # arcsec pixel_scale = 0.2 # arcsec / pixel sky_level = 2.5e3 # counts / arcsec^2 # This time use a particular seed, so the image is deterministic. # This is the same seed that is used in demo2.yaml, which means the images produced # by the two methods will be precisely identical. random_seed = 1534225 logger.info('Starting demo script 2 using:') logger.info(' - sheared (%.2f,%.2f) exponential galaxy (flux = %.1e, scale radius = %.2f),', g1, g2, gal_flux, gal_r0) logger.info(' - circular Moffat PSF (beta = %.1f, re = %.2f),', psf_beta, psf_re) logger.info(' - pixel scale = %.2f,', pixel_scale) logger.info(' - Poisson noise (sky level = %.1e).', sky_level) # Initialize the (pseudo-)random number generator that we will be using below. # For a technical reason that will be explained later (demo9.py), we add 1 to the # given random seed here. rng = galsim.BaseDeviate(random_seed+1) # Define the galaxy profile. gal = galsim.Exponential(flux=gal_flux, scale_radius=gal_r0) # Shear the galaxy by some value. # There are quite a few ways you can use to specify a shape. # q, beta Axis ratio and position angle: q = b/a, 0 < q < 1 # e, beta Ellipticity and position angle: |e| = (1-q^2)/(1+q^2) # g, beta ("Reduced") Shear and position angle: |g| = (1-q)/(1+q) # eta, beta Conformal shear and position angle: eta = ln(1/q) # e1,e2 Ellipticity components: e1 = e cos(2 beta), e2 = e sin(2 beta) # g1,g2 ("Reduced") shear components: g1 = g cos(2 beta), g2 = g sin(2 beta) # eta1,eta2 Conformal shear components: eta1 = eta cos(2 beta), eta2 = eta sin(2 beta) gal = gal.shear(g1=g1, g2=g2) logger.debug('Made galaxy profile') # Define the PSF profile. psf = galsim.Moffat(beta=psf_beta, flux=1., half_light_radius=psf_re) logger.debug('Made PSF profile') # Final profile is the convolution of these. final = galsim.Convolve([gal, psf]) logger.debug('Convolved components into final profile') # Draw the image with a particular pixel scale. image = final.drawImage(scale=pixel_scale) # The "effective PSF" is the PSF as drawn on an image, which includes the convolution # by the pixel response. We label it epsf here. image_epsf = psf.drawImage(scale=pixel_scale) logger.debug('Made image of the profile') # To get Poisson noise on the image, we will use a class called PoissonNoise. # However, we want the noise to correspond to what you would get with a significant # flux from tke sky. This is done by telling PoissonNoise to add noise from a # sky level in addition to the counts currently in the image. # # One wrinkle here is that the PoissonNoise class needs the sky level in each pixel, # while we have a sky_level in counts per arcsec^2. So we need to convert: sky_level_pixel = sky_level * pixel_scale**2 noise = galsim.PoissonNoise(rng, sky_level=sky_level_pixel) image.addNoise(noise) logger.debug('Added Poisson noise') # Write the image to a file. if not os.path.isdir('output'): os.mkdir('output') file_name = os.path.join('output', 'demo2.fits') file_name_epsf = os.path.join('output','demo2_epsf.fits') image.write(file_name) image_epsf.write(file_name_epsf) logger.info('Wrote image to %r',file_name) logger.info('Wrote effective PSF image to %r',file_name_epsf) 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 distortions') 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:') exp_shear = galsim.Shear(g1=g1, g2=g2) logger.info(' g1, g2 = %.3f, %.3f', exp_shear.e1,exp_shear.e2)
def check_crg_noise(n_sed, n_im, n_trial, tol): print("Checking CRG noise for") print("n_sed = {}".format(n_sed)) print("n_im = {}".format(n_im)) print("n_trial = {}".format(n_trial)) print("Constructing chromatic PSFs") in_PSF = galsim.ChromaticAiry(lam=700., diam=2.4) out_PSF = galsim.ChromaticAiry(lam=700., diam=0.6) print("Constructing filters and SEDs") waves = np.arange(550.0, 900.1, 10.0) visband = galsim.Bandpass(galsim.LookupTable(waves, np.ones_like(waves), interpolant='linear'), wave_type='nm') split_points = np.linspace(550.0, 900.0, n_im + 1, endpoint=True) bands = [ visband.truncate(blue_limit=blim, red_limit=rlim) for blim, rlim in zip(split_points[:-1], split_points[1:]) ] maxk = max([ out_PSF.evaluateAtWavelength(waves[0]).maxK(), out_PSF.evaluateAtWavelength(waves[-1]).maxK() ]) SEDs = [ galsim.SED(galsim.LookupTable(waves, waves**i, interpolant='linear'), flux_type='fphotons', wave_type='nm').withFlux(1.0, visband) for i in range(n_sed) ] print("Constructing input noise correlation functions") rng = galsim.BaseDeviate(57721) in_xis = [ galsim.getCOSMOSNoise(cosmos_scale=0.03, rng=rng).dilate(1 + i * 0.05).rotate( 5 * i * galsim.degrees) for i in range(n_im) ] print("Creating noise images") img_sets = [] for i in range(n_trial): imgs = [] for xi in in_xis: img = galsim.Image(128, 128, scale=0.03) img.addNoise(xi) imgs.append(img) img_sets.append(imgs) print("Constructing `ChromaticRealGalaxy`s") crgs = [] for imgs in img_sets: crgs.append( galsim.ChromaticRealGalaxy.makeFromImages(imgs, bands, in_PSF, in_xis, SEDs=SEDs, maxk=maxk)) print("Convolving by output PSF") objs = [galsim.Convolve(crg, out_PSF) for crg in crgs] print("Drawing through output filter") out_imgs = [ obj.drawImage(visband, nx=30, ny=30, scale=0.1) for obj in objs ] noise = objs[0].noise print("Measuring images' correlation functions") xi_obs = galsim.correlatednoise.CorrelatedNoise(out_imgs[0]) for img in out_imgs[1:]: xi_obs += galsim.correlatednoise.CorrelatedNoise(img) xi_obs /= n_trial xi_obs_img = galsim.Image(30, 30, scale=0.1) xi_obs.drawImage(xi_obs_img) noise_img = galsim.Image(30, 30, scale=0.1) noise.drawImage(noise_img) print("Predicted/Observed variance:", noise.getVariance() / xi_obs.getVariance()) print("Predicted/Observed xlag-1 covariance:", noise_img.array[14, 15] / xi_obs_img.array[14, 15]) print("Predicted/Observed ylag-1 covariance:", noise_img.array[15, 14] / xi_obs_img.array[15, 14]) # Just test that the covariances for nearest neighbor pixels are accurate. np.testing.assert_allclose(noise_img.array[14:17, 14:17], xi_obs_img.array[14:17, 14:17], rtol=0, atol=noise.getVariance() * tol)
def test_real_galaxy_ideal(): """Test accuracy of various calculations with fake Gaussian RealGalaxy vs. ideal expectations""" # read in faked Gaussian RealGalaxy from file rgc = galsim.RealGalaxyCatalog(catalog_file, dir=image_dir) assert len(rgc) == rgc.getNObjects() == rgc.nobjects == len(rgc.cat) rg = galsim.RealGalaxy(rgc, index=ind_fake) # as a side note, make sure it behaves okay given a legit RNG and a bad RNG # or when trying to specify the galaxy too many ways rg_1 = galsim.RealGalaxy(rgc, index=ind_fake, rng=galsim.BaseDeviate(1234)) rg_2 = galsim.RealGalaxy(rgc, random=True) try: np.testing.assert_raises(TypeError, galsim.RealGalaxy, rgc, index=ind_fake, rng='foo') np.testing.assert_raises(AttributeError, galsim.RealGalaxy, rgc, index=ind_fake, id=0) np.testing.assert_raises(AttributeError, galsim.RealGalaxy, rgc, index=ind_fake, random=True) np.testing.assert_raises(AttributeError, galsim.RealGalaxy, rgc, id=0, random=True) np.testing.assert_raises(AttributeError, galsim.RealGalaxy, rgc) except ImportError: print('The assert_raises tests require nose') # Different RNGs give different random galaxies. rg_3 = galsim.RealGalaxy(rgc, random=True, rng=galsim.BaseDeviate(12345)) rg_4 = galsim.RealGalaxy(rgc, random=True, rng=galsim.BaseDeviate(67890)) assert rg_3.index != rg_4.index, 'Different seeds did not give different random objects!' check_basic(rg, "RealGalaxy", approx_maxsb=True) check_basic(rg_1, "RealGalaxy", approx_maxsb=True) check_basic(rg_2, "RealGalaxy", approx_maxsb=True) do_pickle( rgc, lambda x: [ x.getGalImage(ind_fake), x.getPSFImage(ind_fake), x.getNoiseProperties(ind_fake) ]) do_pickle( rgc, lambda x: drawNoise(x.getNoise(ind_fake, rng=galsim.BaseDeviate(123)))) do_pickle(rgc) do_pickle( rg, lambda x: [ x.gal_image, x.psf_image, repr(x.noise), x.original_psf.flux, x.original_gal.flux, x.flux ]) do_pickle(rg, lambda x: x.drawImage(nx=20, ny=20, scale=0.7)) do_pickle(rg_1, lambda x: x.drawImage(nx=20, ny=20, scale=0.7)) do_pickle(rg) do_pickle(rg_1) ## for the generation of the ideal right answer, we need to add the intrinsic shape of the ## galaxy and the lensing shear using the rule for addition of distortions which is ugly, but oh ## well: (d1, d2) = galsim.utilities.g1g2_to_e1e2(fake_gal_shear1, fake_gal_shear2) (d1app, d2app) = galsim.utilities.g1g2_to_e1e2(targ_applied_shear1, targ_applied_shear2) denom = 1.0 + d1 * d1app + d2 * d2app dapp_sq = d1app**2 + d2app**2 d1tot = (d1 + d1app + d2app / dapp_sq * (1.0 - np.sqrt(1.0 - dapp_sq)) * (d2 * d1app - d1 * d2app)) / denom d2tot = (d2 + d2app + d1app / dapp_sq * (1.0 - np.sqrt(1.0 - dapp_sq)) * (d1 * d2app - d2 * d1app)) / denom # convolve with a range of Gaussians, with and without shear (note, for this test all the # original and target ePSFs are Gaussian - there's no separate pixel response so that everything # can be calculated analytically) for tps in targ_pixel_scale: for tpf in targ_PSF_fwhm: for tps1 in targ_PSF_shear1: for tps2 in targ_PSF_shear2: print('tps,tpf,tps1,tps2 = ', tps, tpf, tps1, tps2) # make target PSF targ_PSF = galsim.Gaussian(fwhm=tpf).shear(g1=tps1, g2=tps2) # simulate image tmp_gal = rg.withFlux(fake_gal_flux).shear( g1=targ_applied_shear1, g2=targ_applied_shear2) final_tmp_gal = galsim.Convolve(targ_PSF, tmp_gal) sim_image = final_tmp_gal.drawImage(scale=tps, method='no_pixel') # galaxy sigma, in units of pixels on the final image sigma_ideal = (fake_gal_fwhm / tps) * fwhm_to_sigma # compute analytically the expected galaxy moments: mxx_gal, myy_gal, mxy_gal = ellip_to_moments( d1tot, d2tot, sigma_ideal) # compute analytically the expected PSF moments: targ_PSF_e1, targ_PSF_e2 = galsim.utilities.g1g2_to_e1e2( tps1, tps2) targ_PSF_sigma = (tpf / tps) * fwhm_to_sigma mxx_PSF, myy_PSF, mxy_PSF = ellip_to_moments( targ_PSF_e1, targ_PSF_e2, targ_PSF_sigma) # get expected e1, e2, sigma for the PSF-convolved image tot_e1, tot_e2, tot_sigma = moments_to_ellip( mxx_gal + mxx_PSF, myy_gal + myy_PSF, mxy_gal + mxy_PSF) # compare with images that are expected expected_gaussian = galsim.Gaussian(flux=fake_gal_flux, sigma=tps * tot_sigma) expected_gaussian = expected_gaussian.shear(e1=tot_e1, e2=tot_e2) expected_image = galsim.ImageD(sim_image.array.shape[0], sim_image.array.shape[1]) expected_gaussian.drawImage(expected_image, scale=tps, method='no_pixel') printval(expected_image, sim_image) np.testing.assert_array_almost_equal( sim_image.array, expected_image.array, decimal=3, err_msg= "Error in comparison of ideal Gaussian RealGalaxy calculations" )
def test_crg_noise_draw_transform_commutativity(): """Test commutativity of ChromaticRealGalaxy correlated noise under operations of drawImage and applying transformations. """ LSST_i = galsim.Bandpass(os.path.join(bppath, "LSST_r.dat"), 'nm') f606w_cat = galsim.RealGalaxyCatalog('AEGIS_F606w_catalog.fits', dir=image_dir) f814w_cat = galsim.RealGalaxyCatalog('AEGIS_F814w_catalog.fits', dir=image_dir) psf = galsim.Gaussian(fwhm=0.6) crg = galsim.ChromaticRealGalaxy([f606w_cat, f814w_cat], id=14886, maxk=psf.maxK()) factor = 1.5 g1 = g2 = 0.1 mu = 1.2 theta = 45 * galsim.degrees jac = [1.1, 0.1, -0.1, 1.2] orig = galsim.Convolve(crg, psf) orig.drawImage(LSST_i) draw_transform_img = galsim.ImageD(16, 16, scale=0.2) transform_draw_img = draw_transform_img.copy() multiplied = orig * factor multiplied.drawImage(LSST_i) # needed to populate noise property (orig.noise * factor**2).drawImage(image=draw_transform_img) multiplied.noise.drawImage(image=transform_draw_img) np.testing.assert_array_almost_equal(draw_transform_img.array, transform_draw_img.array) divided = orig / factor divided.drawImage(LSST_i) (orig.noise / factor**2).drawImage(image=draw_transform_img) divided.noise.drawImage(image=transform_draw_img) np.testing.assert_array_almost_equal(draw_transform_img.array, transform_draw_img.array) expanded = orig.expand(factor) expanded.drawImage(LSST_i) orig.noise.expand(factor).drawImage(image=draw_transform_img) expanded.noise.drawImage(image=transform_draw_img) np.testing.assert_array_almost_equal(draw_transform_img.array, transform_draw_img.array) dilated = orig.dilate(factor) dilated.drawImage(LSST_i) orig.noise.dilate(factor).drawImage(image=draw_transform_img) dilated.noise.drawImage(image=transform_draw_img) np.testing.assert_array_almost_equal(draw_transform_img.array, transform_draw_img.array) magnified = orig.magnify(mu) magnified.drawImage(LSST_i) orig.noise.magnify(mu).drawImage(image=draw_transform_img) magnified.noise.drawImage(image=transform_draw_img) np.testing.assert_array_almost_equal(draw_transform_img.array, transform_draw_img.array) lensed = orig.lens(g1, g2, mu) lensed.drawImage(LSST_i) orig.noise.lens(g1, g2, mu).drawImage(image=draw_transform_img) lensed.noise.drawImage(image=transform_draw_img) np.testing.assert_array_almost_equal(draw_transform_img.array, transform_draw_img.array) rotated = orig.rotate(theta) rotated.drawImage(LSST_i) orig.noise.rotate(theta).drawImage(image=draw_transform_img) rotated.noise.drawImage(image=transform_draw_img) np.testing.assert_array_almost_equal(draw_transform_img.array, transform_draw_img.array) sheared = orig.shear(g1=g1, g2=g2) sheared.drawImage(LSST_i) orig.noise.shear(g1=g1, g2=g2).drawImage(image=draw_transform_img) sheared.noise.drawImage(image=transform_draw_img) np.testing.assert_array_almost_equal(draw_transform_img.array, transform_draw_img.array) transformed = orig.transform(*jac) transformed.drawImage(LSST_i) orig.noise.transform(*jac).drawImage(image=draw_transform_img) transformed.noise.drawImage(image=transform_draw_img) np.testing.assert_array_almost_equal(draw_transform_img.array, transform_draw_img.array)
def test_area_norm(): """Check that area_norm works as expected""" f606w_cat = galsim.RealGalaxyCatalog('AEGIS_F606w_catalog.fits', dir=image_dir) f814w_cat = galsim.RealGalaxyCatalog('AEGIS_F814w_catalog.fits', dir=image_dir) psf = galsim.Gaussian(fwhm=0.6) rng = galsim.BaseDeviate(5772) crg1 = galsim.ChromaticRealGalaxy([f606w_cat, f814w_cat], random=True, rng=rng.duplicate()) crg2 = galsim.ChromaticRealGalaxy([f606w_cat, f814w_cat], random=True, rng=rng.duplicate(), area_norm=galsim.real.HST_area) assert crg1 != crg2 LSST_i = galsim.Bandpass(os.path.join(bppath, "LSST_r.dat"), 'nm') obj1 = galsim.Convolve(crg1, psf) obj2 = galsim.Convolve(crg2, psf) im1 = obj1.drawImage(LSST_i, exptime=1, area=1) im2 = obj2.drawImage(LSST_i, exptime=1, area=galsim.real.HST_area) printval(im1, im2) np.testing.assert_array_almost_equal(im1.array, im2.array) np.testing.assert_almost_equal( obj1.noise.getVariance(), obj2.noise.getVariance() * galsim.real.HST_area**2) # area_norm is equivalant to an overall scaling crg3 = galsim.ChromaticRealGalaxy([f606w_cat, f814w_cat], random=True, rng=rng.duplicate()) crg3 /= galsim.real.HST_area obj3 = galsim.Convolve(crg3, psf) im3 = obj3.drawImage(LSST_i, exptime=1, area=galsim.real.HST_area) np.testing.assert_array_almost_equal(im3.array, im2.array) np.testing.assert_almost_equal(obj3.noise.getVariance(), obj2.noise.getVariance()) rg1 = galsim.RealGalaxy(f606w_cat, index=1) rg2 = galsim.RealGalaxy(f606w_cat, index=1, area_norm=galsim.real.HST_area) assert rg1 != rg2 obj1 = galsim.Convolve(rg1, psf) obj2 = galsim.Convolve(rg2, psf) im1 = obj1.drawImage() im2 = obj2.drawImage(exptime=1, area=galsim.real.HST_area) printval(im1, im2) np.testing.assert_array_almost_equal(im1.array, im2.array) np.testing.assert_almost_equal( obj1.noise.getVariance(), obj2.noise.getVariance() * galsim.real.HST_area**2) # area_norm is equivalant to an overall scaling rg3 = galsim.RealGalaxy(f606w_cat, index=1) rg3 /= galsim.real.HST_area obj3 = galsim.Convolve(rg3, psf) im3 = obj3.drawImage(exptime=1, area=galsim.real.HST_area) np.testing.assert_array_almost_equal(im3.array, im2.array) np.testing.assert_almost_equal(obj3.noise.getVariance(), obj2.noise.getVariance())
def test_crg_roundtrip_larger_target_psf(): """Test that drawing a chromatic galaxy with a color gradient directly using an LSST-size PSF is equivalent to first drawing the galaxy to HST-like images, and then using ChromaticRealGalaxy to produce an LSST-like image. """ # load some spectra bulge_SED = (galsim.SED( os.path.join(sedpath, 'CWW_E_ext.sed'), wave_type='ang', flux_type='flambda').thin(rel_err=1e-3).withFluxDensity( target_flux_density=0.3, wavelength=500.0)) disk_SED = (galsim.SED( os.path.join(sedpath, 'CWW_Sbc_ext.sed'), wave_type='ang', flux_type='flambda').thin(rel_err=1e-3).withFluxDensity( target_flux_density=0.3, wavelength=500.0)) bulge = galsim.Sersic(n=4, half_light_radius=0.6) * bulge_SED disk = galsim.Sersic(n=1, half_light_radius=0.4) * disk_SED # Decenter components a bit to make the test more complicated disk = disk.shift(0.05, 0.1) gal = (bulge + disk).shear(g1=0.3, g2=0.1) # Much faster to just use some achromatic HST-like PSFs. We'll make them slightly different in # each band though. f606w_PSF = galsim.ChromaticObject(galsim.Gaussian(half_light_radius=0.05)) f814w_PSF = galsim.ChromaticObject(galsim.Gaussian(half_light_radius=0.07)) LSSTPSF = galsim.ChromaticAtmosphere(galsim.Kolmogorov(fwhm=0.7), 600.0, zenith_angle=0.0 * galsim.degrees) f606w = galsim.Bandpass(os.path.join(bppath, "ACS_wfc_F606W.dat"), 'nm').truncate() f814w = galsim.Bandpass(os.path.join(bppath, "ACS_wfc_F814W.dat"), 'nm') LSST_i = galsim.Bandpass(os.path.join(bppath, "LSST_r.dat"), 'nm') truth_image = galsim.Convolve(LSSTPSF, gal).drawImage(LSST_i, nx=24, ny=24, scale=0.2) f606w_image = galsim.Convolve(f606w_PSF, gal).drawImage(f606w, nx=192, ny=192, scale=0.03) f814w_image = galsim.Convolve(f814w_PSF, gal).drawImage(f814w, nx=192, ny=192, scale=0.03) crg = galsim.ChromaticRealGalaxy.makeFromImages( images=[f606w_image, f814w_image], bands=[f606w, f814w], PSFs=[f606w_PSF, f814w_PSF], xis=[galsim.UncorrelatedNoise(1e-16)] * 2, SEDs=[bulge_SED, disk_SED]) test_image = galsim.Convolve(crg, LSSTPSF).drawImage(LSST_i, nx=24, ny=24, scale=0.2) truth_mom = galsim.hsm.FindAdaptiveMom(truth_image) test_mom = galsim.hsm.FindAdaptiveMom(test_image) np.testing.assert_allclose(test_mom.moments_amp, truth_mom.moments_amp, rtol=1e-3, atol=0) np.testing.assert_allclose(test_mom.moments_centroid.x, truth_mom.moments_centroid.x, rtol=0., atol=1e-2) np.testing.assert_allclose(test_mom.moments_centroid.y, truth_mom.moments_centroid.y, rtol=0., atol=1e-2) np.testing.assert_allclose(test_mom.moments_sigma, truth_mom.moments_sigma, rtol=1e-3, atol=0) np.testing.assert_allclose(test_mom.observed_shape.g1, truth_mom.observed_shape.g1, rtol=0, atol=1e-4) np.testing.assert_allclose(test_mom.observed_shape.g2, truth_mom.observed_shape.g2, rtol=0, atol=1e-4)
def test_dcr(): """Test the dcr surface op """ # This tests that implementing DCR with the surface op is equivalent to using # ChromaticAtmosphere. # We use fairly extreme choices for the parameters to make the comparison easier, so # we can still get good discrimination of any errors with only 10^6 photons. zenith_angle = 45 * galsim.degrees # Larger angle has larger DCR. parallactic_angle = 129 * galsim.degrees # Something random, not near 0 or 180 pixel_scale = 0.03 # Small pixel scale means shifts are many pixels, rather than a fraction. alpha = -1.2 # The normal alpha is -0.2, so this is exaggerates the effect. bandpass = galsim.Bandpass('LSST_r.dat', 'nm') base_wavelength = bandpass.effective_wavelength base_wavelength += 500 # This exaggerates the effects fairly substantially. sed = galsim.SED('CWW_E_ext.sed', wave_type='ang', flux_type='flambda') flux = 1.e6 base_PSF = galsim.Kolmogorov(fwhm=0.3) # Use ChromaticAtmosphere im1 = galsim.ImageD(50, 50, scale=pixel_scale) star = galsim.DeltaFunction() * sed star = star.withFlux(flux, bandpass=bandpass) chrom_PSF = galsim.ChromaticAtmosphere(base_PSF, base_wavelength=base_wavelength, zenith_angle=zenith_angle, parallactic_angle=parallactic_angle, alpha=alpha) chrom = galsim.Convolve(star, chrom_PSF) chrom.drawImage(bandpass, image=im1) # Use PhotonDCR im2 = galsim.ImageD(50, 50, scale=pixel_scale) dcr = galsim.PhotonDCR(base_wavelength=base_wavelength, zenith_angle=zenith_angle, parallactic_angle=parallactic_angle, alpha=alpha) achrom = base_PSF.withFlux(flux) rng = galsim.BaseDeviate(31415) wave_sampler = galsim.WavelengthSampler(sed, bandpass, rng) photon_ops = [wave_sampler, dcr] achrom.drawImage(image=im2, method='phot', rng=rng, photon_ops=photon_ops) do_pickle(dcr) im1 /= flux # Divide by flux, so comparison is on a relative basis. im2 /= flux printval(im2, im1, show=False) np.testing.assert_almost_equal(im2.array, im1.array, decimal=4, err_msg="PhotonDCR didn't match ChromaticAtmosphere") # Repeat with thinned bandpass and SED to check that thin still works well. im3 = galsim.ImageD(50, 50, scale=pixel_scale) thin = 0.1 # Even higher also works. But this is probably enough. thin_bandpass = bandpass.thin(thin) thin_sed = sed.thin(thin) print('len bp = %d => %d'%(len(bandpass.wave_list), len(thin_bandpass.wave_list))) print('len sed = %d => %d'%(len(sed.wave_list), len(thin_sed.wave_list))) wave_sampler = galsim.WavelengthSampler(thin_sed, thin_bandpass, rng) achrom.drawImage(image=im3, method='phot', rng=rng, photon_ops=photon_ops) im3 /= flux printval(im3, im1, show=False) np.testing.assert_almost_equal(im3.array, im1.array, decimal=4, err_msg="thinning factor %f led to 1.e-4 level inaccuracy"%thin) # Check scale_unit im4 = galsim.ImageD(50, 50, scale=pixel_scale/60) dcr = galsim.PhotonDCR(base_wavelength=base_wavelength, zenith_angle=zenith_angle, parallactic_angle=parallactic_angle, scale_unit='arcmin', alpha=alpha) photon_ops = [wave_sampler, dcr] achrom.dilate(1./60).drawImage(image=im4, method='phot', rng=rng, photon_ops=photon_ops) im4 /= flux printval(im4, im1, show=False) np.testing.assert_almost_equal(im4.array, im1.array, decimal=4, err_msg="PhotonDCR with scale_unit=arcmin, didn't match") # Check some other valid options # alpha = 0 means don't do any size scaling. # obj_coord, HA and latitude are another option for setting the angles # pressure, temp, and water pressure are settable. # Also use a non-trivial WCS. wcs = galsim.FitsWCS('des_data/DECam_00154912_12_header.fits') image = galsim.Image(50, 50, wcs=wcs) bandpass = galsim.Bandpass('LSST_r.dat', wave_type='nm').thin(0.1) base_wavelength = bandpass.effective_wavelength lsst_lat = galsim.Angle.from_dms('-30:14:23.76') lsst_long = galsim.Angle.from_dms('-70:44:34.67') local_sidereal_time = 3.14 * galsim.hours # Not pi. This is the time for this observation. im5 = galsim.ImageD(50, 50, wcs=wcs) obj_coord = wcs.toWorld(im5.true_center) base_PSF = galsim.Kolmogorov(fwhm=0.9) achrom = base_PSF.withFlux(flux) dcr = galsim.PhotonDCR(base_wavelength=bandpass.effective_wavelength, obj_coord=obj_coord, HA=local_sidereal_time-obj_coord.ra, latitude=lsst_lat, pressure=72, # default is 69.328 temperature=290, # default is 293.15 H2O_pressure=0.9) # default is 1.067 #alpha=0) # default is 0, so don't need to set it. photon_ops = [wave_sampler, dcr] achrom.drawImage(image=im5, method='phot', rng=rng, photon_ops=photon_ops) do_pickle(dcr) im6 = galsim.ImageD(50, 50, wcs=wcs) star = galsim.DeltaFunction() * sed star = star.withFlux(flux, bandpass=bandpass) chrom_PSF = galsim.ChromaticAtmosphere(base_PSF, base_wavelength=bandpass.effective_wavelength, obj_coord=obj_coord, HA=local_sidereal_time-obj_coord.ra, latitude=lsst_lat, pressure=72, temperature=290, H2O_pressure=0.9, alpha=0) chrom = galsim.Convolve(star, chrom_PSF) chrom.drawImage(bandpass, image=im6) im5 /= flux # Divide by flux, so comparison is on a relative basis. im6 /= flux printval(im5, im6, show=False) np.testing.assert_almost_equal(im5.array, im6.array, decimal=3, err_msg="PhotonDCR with alpha=0 didn't match") # Also check invalid parameters zenith_coord = galsim.CelestialCoord(13.54 * galsim.hours, lsst_lat) assert_raises(TypeError, galsim.PhotonDCR, zenith_angle=zenith_angle, parallactic_angle=parallactic_angle) # base_wavelength is required assert_raises(TypeError, galsim.PhotonDCR, base_wavelength=500, parallactic_angle=parallactic_angle) # zenith_angle (somehow) is required assert_raises(TypeError, galsim.PhotonDCR, 500, zenith_angle=34.4, parallactic_angle=parallactic_angle) # zenith_angle must be Angle assert_raises(TypeError, galsim.PhotonDCR, 500, zenith_angle=zenith_angle, parallactic_angle=34.5) # parallactic_angle must be Angle assert_raises(TypeError, galsim.PhotonDCR, 500, obj_coord=obj_coord, latitude=lsst_lat) # Missing HA assert_raises(TypeError, galsim.PhotonDCR, 500, obj_coord=obj_coord, HA=local_sidereal_time-obj_coord.ra) # Missing latitude assert_raises(TypeError, galsim.PhotonDCR, 500, obj_coord=obj_coord) # Need either zenith_coord, or (HA,lat) assert_raises(TypeError, galsim.PhotonDCR, 500, obj_coord=obj_coord, zenith_coord=zenith_coord, HA=local_sidereal_time-obj_coord.ra) # Can't have both HA and zenith_coord assert_raises(TypeError, galsim.PhotonDCR, 500, obj_coord=obj_coord, zenith_coord=zenith_coord, latitude=lsst_lat) # Can't have both lat and zenith_coord assert_raises(TypeError, galsim.PhotonDCR, 500, zenith_angle=zenith_angle, parallactic_angle=parallactic_angle, H20_pressure=1.) # invalid (misspelled) assert_raises(ValueError, galsim.PhotonDCR, 500, zenith_angle=zenith_angle, parallactic_angle=parallactic_angle, scale_unit='inches') # invalid scale_unit # Invalid to use dcr without some way of setting wavelengths. assert_raises(galsim.GalSimError, achrom.drawImage, im2, method='phot', photon_ops=[dcr])
def test_CRG_noise(args): """Test noise propagation in ChromaticRealGalaxy """ t0 = time.time() print("Constructing chromatic PSFs") in_PSF = galsim.ChromaticAiry(lam=700., diam=2.4) out_PSF = galsim.ChromaticAiry(lam=700., diam=1.2) print("Constructing filters and SEDs") waves = np.arange(550.0, 900.1, 10.0) visband = galsim.Bandpass(galsim.LookupTable(waves, np.ones_like(waves), interpolant='linear'), wave_type='nm') split_points = np.linspace(550.0, 900.0, args.Nim + 1, endpoint=True) bands = [ visband.truncate(blue_limit=blim, red_limit=rlim) for blim, rlim in zip(split_points[:-1], split_points[1:]) ] maxk = max([ out_PSF.evaluateAtWavelength(waves[0]).maxK(), out_PSF.evaluateAtWavelength(waves[-1]).maxK() ]) SEDs = [ galsim.SED(galsim.LookupTable(waves, waves**i, interpolant='linear'), flux_type='fphotons', wave_type='nm').withFlux(1.0, visband) for i in range(args.NSED) ] print("Constructing input noise correlation functions") rng = galsim.BaseDeviate(args.seed) in_xis = [ galsim.getCOSMOSNoise(cosmos_scale=args.in_scale, rng=rng).dilate(1 + i * 0.05).rotate( 5 * i * galsim.degrees) for i in range(args.Nim) ] print("Creating noise images") img_sets = [] for i in range(args.Ntrial): imgs = [] for j, xi in enumerate(in_xis): img = galsim.Image(args.in_Nx, args.in_Nx, scale=args.in_scale) img.addNoise(xi) imgs.append(img) img_sets.append(imgs) print("Constructing `ChromaticRealGalaxy`s") crgs = [] with ProgressBar(len(img_sets)) as bar: for imgs in img_sets: crgs.append( galsim.ChromaticRealGalaxy.makeFromImages(imgs, bands, in_PSF, in_xis, SEDs=SEDs, maxk=maxk)) bar.update() print("Convolving by output PSF") objs = [galsim.Convolve(crg, out_PSF) for crg in crgs] print("Drawing through output filter") out_imgs = [ obj.drawImage(visband, nx=args.out_Nx, ny=args.out_Nx, scale=args.out_scale, iimult=args.iimult) for obj in objs ] noise = objs[0].noise print("Measuring images' correlation functions") xi_obs = galsim.correlatednoise.CorrelatedNoise(out_imgs[0]) for img in out_imgs[1:]: xi_obs += galsim.correlatednoise.CorrelatedNoise(img) xi_obs /= args.Ntrial xi_obs_img = galsim.Image(args.out_Nx, args.out_Nx, scale=args.out_scale) xi_obs.drawImage(xi_obs_img) print("Observed image variance: ", xi_obs.getVariance()) print("Predicted image variance: ", noise.getVariance()) print("Predicted/Observed variance:", noise.getVariance() / xi_obs.getVariance()) print("Took {} seconds".format(time.time() - t0)) if args.plot: import matplotlib.pyplot as plt out_array = (np.arange(args.out_Nx) - args.out_Nx / 2) * args.out_scale out_extent = [ -args.out_Nx * args.out_scale / 2, args.out_Nx * args.out_scale / 2, -args.out_Nx * args.out_scale / 2, args.out_Nx * args.out_scale / 2 ] fig = plt.figure(figsize=(5, 5)) # Sample image ax = fig.add_subplot(111) ax.imshow(out_imgs[0].array, extent=out_extent) ax.set_title("sample output image") ax.set_xlabel("x") ax.set_ylabel("y") # ax.colorbar() fig.show() # 2D correlation functions fig = plt.figure(figsize=(10, 10)) ax1 = fig.add_subplot(221) noise_img = galsim.Image(args.out_Nx, args.out_Nx, scale=args.out_scale) noise.drawImage(noise_img) ax1.imshow(np.log10(np.abs(noise_img.array)), extent=out_extent) ax1.set_title("predicted covariance function") ax1.set_xlabel(r"$\Delta x$") ax1.set_ylabel(r"$\Delta y$") ax2 = fig.add_subplot(222) ax2.imshow(np.log10(np.abs(xi_obs_img.array)), extent=out_extent) ax2.set_title("observed covariance function") ax2.set_xlabel(r"$\Delta x$") ax2.set_ylabel(r"$\Delta y$") # 1D slide through correlation functions ax3 = fig.add_subplot(223) ax3.plot(out_array, noise_img.array[args.out_Nx / 2, :], label="prediction", color='red') ax3.plot(out_array, xi_obs_img.array[args.out_Nx / 2, :], label="observation", color='blue') ax3.legend(loc='best') ax3.set_xlabel(r"$\Delta x$") ax3.set_ylabel(r"$\xi$") ax4 = fig.add_subplot(224) ax4.plot(out_array, noise_img.array[args.out_Nx / 2, :], label="prediction", color='red') ax4.plot(out_array, xi_obs_img.array[args.out_Nx / 2, :], label="observation", color='blue') ax4.plot(out_array, -noise_img.array[args.out_Nx / 2, :], ls=':', color='red') ax4.plot(out_array, -xi_obs_img.array[args.out_Nx / 2, :], ls=':', color='blue') ax4.legend(loc='best') ax4.set_yscale('log') ax4.set_xlabel(r"$\Delta x$") ax4.set_ylabel(r"$\xi$") plt.tight_layout() plt.show()
def main(): # Using very low accuracy GSParams here for speed gsparams = galsim.GSParams( minimum_fft_size=256, folding_threshold=0.1, kvalue_accuracy=1e-3, stepk_minimum_hlr=2.5, ) # Note - we actually use an interpolated image instead; just putting this in # so you can run the code without needing that file psf_prof = galsim.OpticalPSF( lam=725, # nm diam=1.2, # m defocus=0, obscuration=0.33, nstruts=3, gsparams=gsparams) pixel_scale = 0.02 convolved_image = galsim.Image(256, 256, scale=pixel_scale) convolved_image.setCenter(0, 0) # Do this once here to get the right kimage size/shape and wrap size. gal_prof = galsim.Sersic(n=4, half_light_radius=0.3, gsparams=gsparams) convolved_prof = galsim.Convolve(gal_prof, psf_prof, gsparams=gsparams) psf_kimage, wrap_size = convolved_prof.drawFFT_makeKImage(convolved_image) # Draw the PSF onto the kimage. psf_prof._drawKImage(psf_kimage) # Use the same size/shape for the galaxy part. gal_kimage = psf_kimage.copy() convolved_image2 = convolved_image.copy() for _i in range(1000): gal_prof = galsim.Sersic(n=4, half_light_radius=0.3, gsparams=gsparams) # Account for the fact that this is an even sized image. The drawFFT function will # draw the profile centered on the nominal (integer) center pixel, which (since this is # an even-sized image) actuall +0.5,+0.5 from the true center. gal_prof_cen = gal_prof._shift( galsim.PositionD(-0.5 * pixel_scale, -0.5 * pixel_scale)) # Draw just the galaxy profile in k-space gal_prof_cen._drawKImage(gal_kimage) # Multiply by the (constant) PSF kimage gal_kimage.array[:, :] *= psf_kimage.array # Finish the draw process gal_prof.drawFFT_finish(convolved_image, gal_kimage, wrap_size, add_to_image=False) if False: # Check that we get the same thing as the normal draw procedure convolved_prof = galsim.Convolve(gal_prof, psf_prof, gsparams=gsparams) # Using no pixel method here since we plan to use a PSF profile # which already includes the pixel response convolved_prof.drawImage(convolved_image2, method='no_pixel') max_diff = np.max( np.abs(convolved_image.array - convolved_image2.array)) print('max diff = ', max_diff) assert (max_diff < 1.e-8)
def test_spergel(): """Test the generation of a specific Spergel profile against a known result. """ test_spergel_nu = [-0.85, -0.5, 0.0, 0.85, 4.0] mathica_enclosed_fluxes = [ 3.06256e-2, 9.99995e-6, 6.06443e-10, 2.94117e-11, 6.25011e-12 ] mathica_enclosing_radii = [ 2.3973e-17, 1.00001e-5, 1.69047e-3, 5.83138e-3, 1.26492e-2 ] for nu, enclosed_flux, enclosing_radius in zip(test_spergel_nu, mathica_enclosed_fluxes, mathica_enclosing_radii): filename = "spergel_nu{0:.2f}.fits".format(nu) savedImg = galsim.fits.read(os.path.join(imgdir, filename)) savedImg.setCenter(0, 0) dx = 0.2 myImg = galsim.ImageF(savedImg.bounds, scale=dx) myImg.setCenter(0, 0) spergel = galsim.Spergel(nu=nu, half_light_radius=1.0) # Reference images were made with old centering, # which is equivalent to use_true_center=False. myImg = spergel.drawImage(myImg, scale=dx, method="sb", use_true_center=False) np.testing.assert_array_almost_equal( myImg.array, savedImg.array, 5, err_msg="Using GSObject Spergel disagrees with expected result") gsp = galsim.GSParams(xvalue_accuracy=1.e-8, kvalue_accuracy=1.e-8) spergel2 = galsim.Spergel(nu=nu, half_light_radius=1.0, gsparams=gsp) assert spergel2 != spergel assert spergel2 == spergel.withGSParams(gsp) # nu < 0 has inf for xValue(0,0), so the x tests fail for them. check_basic(spergel, "Spergel with nu=%f" % nu, do_x=(nu > 0)) # Only nu >= -0.3 give reasonably sized FFTs, # and small nu method='phot' is super slow. if nu >= -0.3: test_im = galsim.Image(16, 16, scale=dx) do_kvalue(spergel, test_im, "Spergel(nu={0:1}) ".format(nu)) # Test photon shooting. # Convolve with a small gaussian to smooth out the central peak. spergel2 = galsim.Convolve(spergel, galsim.Gaussian(sigma=0.3)) do_shoot(spergel2, myImg, "Spergel") # Test integrated flux routines against Mathematica spergel = galsim.Spergel(nu=nu, scale_radius=1.0) np.testing.assert_almost_equal( spergel.calculateFluxRadius(1.e-5) / enclosing_radius, 1.0, 4, err_msg="Calculated incorrect Spergel(nu={0}) flux-enclosing-radius." .format(nu)) np.testing.assert_almost_equal( spergel.calculateIntegratedFlux(1.e-5) / enclosed_flux, 1.0, 4, err_msg="Calculated incorrect Spergel(nu={0}) enclosed flux.". format(nu)) # Use non-unity values. spergel = galsim.Spergel(nu=0.37, flux=1.7, half_light_radius=2.3) check_basic(spergel, "Spergel") # Check picklability do_pickle(spergel, lambda x: x.drawImage(method='no_pixel')) do_pickle(spergel) do_pickle(galsim.Spergel(0, 1)) # Should raise an exception if both scale_radius and half_light_radius are provided. assert_raises(TypeError, galsim.Spergel, nu=0, scale_radius=3, half_light_radius=1) assert_raises(TypeError, galsim.Spergel, nu=0) assert_raises(TypeError, galsim.Spergel, scale_radius=3) # Allowed range = [-0.85, 4.0] assert_raises(ValueError, galsim.Spergel, nu=-0.9) assert_raises(ValueError, galsim.Spergel, nu=4.1)
def main(argv): """ Make images using variable PSF and shear: - The main image is 10 x 10 postage stamps. - Each postage stamp is 48 x 48 pixels. - The second HDU has the corresponding PSF image. - Applied shear is from a power spectrum P(k) ~ k^1.8. - Galaxies are real galaxies oriented in a ring test of 20 each. - The PSF is Gaussian with FWHM, ellipticity and position angle functions of (x,y) - Noise is Poisson using a nominal sky value of 1.e6. """ logging.basicConfig(format="%(message)s", level=logging.INFO, stream=sys.stdout) logger = logging.getLogger("demo10") # Define some parameters we'll use below. # Normally these would be read in from some parameter file. n_tiles = 10 # number of tiles in each direction. stamp_size = 48 # pixels pixel_scale = 0.44 # arcsec / pixel sky_level = 1.e6 # ADU / arcsec^2 # The random seed is used for both the power spectrum realization and the random properties # of the galaxies. random_seed = 3339201 # Make output directory if not already present. if not os.path.isdir('output'): os.mkdir('output') file_name = os.path.join('output', 'power_spectrum.fits') # These will be created for each object below. The values we'll use will be functions # of (x,y) relative to the center of the image. (r = sqrt(x^2+y^2)) # psf_fwhm = 0.9 + 0.5 * (r/100)^2 -- arcsec # psf_e = 0.4 * (r/100)^1.5 -- large value at the edge, so visible by eye. # psf_beta = atan2(y/x) + pi/2 -- tangential pattern gal_dilation = 3 # Make the galaxies a bit larger than their original size. gal_signal_to_noise = 100 # Pretty high. psf_signal_to_noise = 1000 # Even higher. logger.info('Starting demo script 10') # Read in galaxy catalog cat_file_name = 'real_galaxy_catalog_23.5_example.fits' dir = 'data' real_galaxy_catalog = galsim.RealGalaxyCatalog(cat_file_name, dir=dir) logger.info('Read in %d real galaxies from catalog', real_galaxy_catalog.nobjects) # List of IDs to use. We select 5 particularly irregular galaxies for this demo. # Then we'll choose randomly from this list. id_list = [106416, 106731, 108402, 116045, 116448] # Make the 5 galaxies we're going to use here rather than remake them each time. # This means the Fourier transforms of the real galaxy images don't need to be recalculated # each time, so it's a bit more efficient. gal_list = [ galsim.RealGalaxy(real_galaxy_catalog, id=id) for id in id_list ] # Grab the index numbers before we transform them and lose the index attribute. cosmos_index = [gal.index for gal in gal_list] # Make the galaxies a bit larger than their original observed size. gal_list = [gal.dilate(gal_dilation) for gal in gal_list] # Setup the PowerSpectrum object we'll be using: ps = galsim.PowerSpectrum(lambda k: k**1.8) # The argument here is "e_power_function" which defines the E-mode power to use. # There is also a b_power_function if you want to include any B-mode power: # ps = galsim.PowerSpectrum(e_power_function, b_power_function) # You may even omit the e_power_function argument and have a pure B-mode power spectrum. # ps = galsim.PowerSpectrum(b_power_function = b_power_function) # All the random number generator classes derive from BaseDeviate. # When we construct another kind of deviate class from any other # kind of deviate class, the two share the same underlying random number # generator. Sometimes it can be clearer to just construct a BaseDeviate # explicitly and then construct anything else you need from that. # Note: A BaseDeviate cannot be used to generate any values. It can # only be used in the constructor for other kinds of deviates. # The seeds for the objects are random_seed+1..random_seed+nobj. # The seeds for things at the image or file level use random_seed itself. nobj = n_tiles * n_tiles rng = galsim.BaseDeviate(random_seed) # Have the PowerSpectrum object build a grid of shear values for us to use. grid_g1, grid_g2 = ps.buildGrid(grid_spacing=stamp_size * pixel_scale, ngrid=n_tiles + 1, rng=rng) # Setup the images: gal_image = galsim.ImageF(stamp_size * n_tiles, stamp_size * n_tiles) psf_image = galsim.ImageF(stamp_size * n_tiles, stamp_size * n_tiles) # Update the image WCS to use the image center as the origin of the WCS. # The class that acts like a PixelScale except for this offset is called OffsetWCS. im_center = gal_image.true_center wcs = galsim.OffsetWCS(scale=pixel_scale, origin=im_center) gal_image.wcs = wcs psf_image.wcs = wcs # We will place the tiles in a random order. To do this, we make two lists for the # ix and iy values. Then we apply a random permutation to the lists (in tandem). ix_list = [] iy_list = [] for ix in range(n_tiles): for iy in range(n_tiles): ix_list.append(ix) iy_list.append(iy) # This next function will use the given random number generator, rng, and use it to # randomly permute any number of lists. All lists will have the same random permutation # applied. galsim.random.permute(rng, ix_list, iy_list) # Initialize the OutputCatalog for the truth values names = [ 'gal_num', 'x_image', 'y_image', 'psf_e1', 'psf_e2', 'psf_fwhm', 'cosmos_id', 'cosmos_index', 'theta', 'g1', 'g2', 'shift_x', 'shift_y' ] types = [ int, float, float, float, float, float, str, int, float, float, float, float, float ] truth_catalog = galsim.OutputCatalog(names, types) # Build each postage stamp: for k in range(nobj): # The usual random number generator using a different seed for each galaxy. rng = galsim.BaseDeviate(random_seed + k + 1) # Determine the bounds for this stamp and its center position. ix = ix_list[k] iy = iy_list[k] b = galsim.BoundsI(ix * stamp_size + 1, (ix + 1) * stamp_size, iy * stamp_size + 1, (iy + 1) * stamp_size) sub_gal_image = gal_image[b] sub_psf_image = psf_image[b] pos = wcs.toWorld(b.true_center) # The image comes out as about 211 arcsec across, so we define our variable # parameters in terms of (r/100 arcsec), so roughly the scale size of the image. rsq = (pos.x**2 + pos.y**2) r = math.sqrt(rsq) psf_fwhm = 0.9 + 0.5 * rsq / 100**2 # arcsec psf_e = 0.4 * (r / 100.)**1.5 psf_beta = (math.atan2(pos.y, pos.x) + math.pi / 2) * galsim.radians # Define the PSF profile psf = galsim.Gaussian(fwhm=psf_fwhm) psf_shape = galsim.Shear(e=psf_e, beta=psf_beta) psf = psf.shear(psf_shape) # Define the galaxy profile: # For this demo, we are doing a ring test where the same galaxy profile is drawn at many # orientations stepped uniformly in angle, making a ring in e1-e2 space. # We're drawing each profile at 20 different orientations and then skipping to the # next galaxy in the list. So theta steps by 1/20 * 360 degrees: theta_deg = (k % 20) * 360. / 20 theta = theta_deg * galsim.degrees # The index needs to increment every 20 objects so we use k/20 using integer math. index = k // 20 gal = gal_list[index] # This makes a new copy so we're not changing the object in the gal_list. gal = gal.rotate(theta) # Apply the shear from the power spectrum. We should either turn the gridded shears # grid_g1[iy, ix] and grid_g2[iy, ix] into gridded reduced shears using a utility called # galsim.lensing.theoryToObserved, or use ps.getShear() which by default gets the reduced # shear. ps.getShear() is also more flexible because it can get the shear at positions that # are not on the original grid, as long as they are contained within the bounds of the full # grid. So in this example we'll use ps.getShear(). alt_g1, alt_g2 = ps.getShear(pos) gal = gal.shear(g1=alt_g1, g2=alt_g2) # Apply half-pixel shift in a random direction. shift_r = pixel_scale * 0.5 ud = galsim.UniformDeviate(rng) t = ud() * 2. * math.pi dx = shift_r * math.cos(t) dy = shift_r * math.sin(t) gal = gal.shift(dx, dy) # Make the final image, convolving with the psf final = galsim.Convolve([psf, gal]) # Draw the image final.drawImage(sub_gal_image) # For the PSF image, we don't match the galaxy shift. Rather, we use the offset # parameter to drawImage to apply a random offset of up to 0.5 pixels in each direction. # Note the difference in units between shift and offset. The shift is applied to the # surface brightness profile, so it is in sky coordinates (as all dimension are for # GSObjects), which are arcsec here. The offset though is applied to the image itself, # so it is in pixels. Hence, we don't multiply by pixel_scale. psf_dx = ud() - 0.5 psf_dy = ud() - 0.5 psf_offset = galsim.PositionD(psf_dx, psf_dy) # Draw the PSF image: # We use real space integration over the pixels to avoid some of the # artifacts that can show up with Fourier convolution. # The level of the artifacts is quite low, but when drawing with # so little noise, they are apparent with ds9's zscale viewing. psf.drawImage(sub_psf_image, method='real_space', offset=psf_offset) # Build the noise model: Poisson noise with a given sky level. sky_level_pixel = sky_level * pixel_scale**2 noise = galsim.PoissonNoise(rng, sky_level=sky_level_pixel) # Add noise to the PSF image, using the normal noise model, but scaling the # PSF flux high enough to reach the desired signal-to-noise. # See demo5.py for more info about how this works. sub_psf_image.addNoiseSNR(noise, psf_signal_to_noise) # And also to the galaxy image using its signal-to-noise. sub_gal_image.addNoiseSNR(noise, gal_signal_to_noise) # Add the truth values to the truth catalog row = [ k, b.true_center.x, b.true_center.y, psf_shape.e1, psf_shape.e2, psf_fwhm, id_list[index], cosmos_index[index], (theta_deg % 360.), alt_g1, alt_g2, dx, dy ] truth_catalog.addRow(row) logger.info('Galaxy (%d,%d): position relative to center = %s', ix, iy, str(pos)) logger.info('Done making images of postage stamps') # In this case, we'll attach the truth catalog as an additional HDU in the same file as # the image data. truth_hdu = truth_catalog.writeFitsHdu() # Now write the images to disk. images = [gal_image, psf_image, truth_hdu] # Any items in the "images" list that is already an hdu is just used directly. # The actual images are converted to FITS hdus that contain the image data. galsim.fits.writeMulti(images, file_name) logger.info('Wrote image to %r', file_name)
def main(argv): """ Make a fits image cube using real COSMOS galaxies from a catalog describing the training sample. - The number of images in the cube matches the number of rows in the catalog. - Each image size is computed automatically by GalSim based on the Nyquist size. - Both galaxies and stars. - PSF is a double Gaussian, the same for each galaxy. - Galaxies are randomly rotated to remove the imprint of any lensing shears in the COSMOS data. - The same shear is applied to each galaxy. - Noise is Poisson using a nominal sky value of 1.e6 ADU/arcsec^2, the noise in the original COSMOS data. """ logging.basicConfig(format="%(message)s", level=logging.INFO, stream=sys.stdout) logger = logging.getLogger("demo6") # Define some parameters we'll use below. cat_file_name = 'real_galaxy_catalog_example.fits' dir = 'data' # Make output directory if not already present. if not os.path.isdir('output'): os.mkdir('output') cube_file_name = os.path.join('output', 'cube_real.fits') psf_file_name = os.path.join('output', 'psf_real.fits') random_seed = 1512413 sky_level = 1.e6 # ADU / arcsec^2 pixel_scale = 0.16 # arcsec gal_flux = 1.e5 # arbitrary choice, makes nice (not too) noisy images gal_g1 = -0.027 # gal_g2 = 0.031 # gal_mu = 1.082 # mu = ( (1-kappa)^2 - g1^2 - g2^2 )^-1 psf_inner_fwhm = 0.6 # arcsec psf_outer_fwhm = 2.3 # arcsec psf_inner_fraction = 0.8 # fraction of total PSF flux in the inner Gaussian psf_outer_fraction = 0.2 # fraction of total PSF flux in the inner Gaussian ngal = 100 logger.info('Starting demo script 6 using:') logger.info(' - real galaxies from catalog %r', cat_file_name) logger.info(' - double Gaussian PSF') logger.info(' - pixel scale = %.2f', pixel_scale) logger.info(' - Applied gravitational shear = (%.3f,%.3f)', gal_g1, gal_g2) logger.info(' - Poisson noise (sky level = %.1e).', sky_level) # Read in galaxy catalog # Note: dir is the directory both for the catalog itself and also the directory prefix # for the image files listed in the catalog. # If the images are in a different directory, you may also specify image_dir, which gives # the relative path from dir to wherever the images are located. real_galaxy_catalog = galsim.RealGalaxyCatalog(cat_file_name, dir=dir) logger.info('Read in %d real galaxies from catalog', real_galaxy_catalog.nobjects) # Make the double Gaussian PSF psf1 = galsim.Gaussian(fwhm=psf_inner_fwhm, flux=psf_inner_fraction) psf2 = galsim.Gaussian(fwhm=psf_outer_fwhm, flux=psf_outer_fraction) psf = psf1 + psf2 # Draw the PSF with no noise. psf_image = psf.drawImage(scale=pixel_scale) # write to file psf_image.write(psf_file_name) logger.info('Created PSF and wrote to file %r', psf_file_name) # Build the images all_images = [] for k in range(ngal): logger.debug('Start work on image %d', k) t1 = time.time() # Initialize the random number generator we will be using. rng = galsim.UniformDeviate(random_seed + k) gal = galsim.RealGalaxy(real_galaxy_catalog, index=k) logger.debug(' Read in training sample galaxy and PSF from file') t2 = time.time() # Set the flux gal = gal.withFlux(gal_flux) # Rotate by a random angle theta = 2. * math.pi * rng() * galsim.radians gal = gal.rotate(theta) # Apply the desired shear gal = gal.shear(g1=gal_g1, g2=gal_g2) # Also apply a magnification mu = ( (1-kappa)^2 - |gamma|^2 )^-1 # This conserves surface brightness, so it scales both the area and flux. gal = gal.magnify(gal_mu) # Make the combined profile final = galsim.Convolve([psf, gal]) # Offset by up to 1/2 pixel in each direction # We had previously (in demo4 and demo5) used shift(dx,dy) as a way to shift the center of # the image. Since that is applied to the galaxy, the units are arcsec (since the galaxy # profile itself doesn't know about the pixel scale). Here, the offset applies to the # drawn image, which does know about the pixel scale, so the units of offset are pixels, # not arcsec. Here, we apply an offset of up to half a pixel in each direction. dx = rng() - 0.5 dy = rng() - 0.5 # Draw the profile if k == 0: # Note that the offset argument may be a galsim.PositionD object or a tuple (dx,dy). im = final.drawImage(scale=pixel_scale, offset=(dx, dy)) xsize, ysize = im.array.shape else: im = galsim.ImageF(xsize, ysize) final.drawImage(im, scale=pixel_scale, offset=(dx, dy)) logger.debug(' Drew image') t3 = time.time() # Add a constant background level background = sky_level * pixel_scale**2 im += background # Add Poisson noise. This time, we don't give a sky_level, since we have already # added it to the image, so we don't want any more added. The sky_level parameter # really defines how much _extra_ sky should be added above what is already in the image. im.addNoise(galsim.PoissonNoise(rng)) logger.debug(' Added Poisson noise') t4 = time.time() # Store that into the list of all images all_images += [im] t5 = time.time() logger.debug(' Times: %f, %f, %f, %f', t2 - t1, t3 - t2, t4 - t3, t5 - t4) logger.info('Image %d: size = %d x %d, total time = %f sec', k, xsize, ysize, t5 - t1) logger.info('Done making images of galaxies') # Now write the image to disk. # We write the images to a fits data cube. galsim.fits.writeCube(all_images, cube_file_name) logger.info('Wrote image to fits data cube %r', cube_file_name)
def generate_sample(args): """Generate one valid sample and write it to the destination arrays. Args (packed as a tuple): i: index of the current sample sersic_index: an optional value of the Sersic Index (default: None, i.e. random) psf_re: an optional PSF (default: None, i.e. random) noise: an optional Gaussian noise level (default: None, i.e. random) """ # Unpack the arguments i, sersic_index, psf_re, noise = args # Loop until a sample satisfies all criteria while True: with counter.get_lock(): # Increment the global iteration counter counter.value += 1 # Initialize the random number generators random.seed(random_seed + counter.value) rng = galsim.BaseDeviate(random_seed + counter.value + 1) # PSF Moffat full-width-half-max of the profile: fixed vs random if psf_re is None: psf_re = random.uniform(0.5, 1) random_psf = True else: random_psf = False # Gaussian noise level: fixed vs random noise = random.randint(200, 400) if noise is None else noise # Sersic index: discrete vs continuous bulge_n = random.uniform(0.5, 6) if sersic_index is None else sersic_index # Sersic radius, unit arcsec bulge_re = random.uniform(0.1, 0.6) # q is ellipticity and beta is orientation. # You could directly predict q and beta but there would be a discontiniuty issue # for beta. A jump from 180 degree to 1 degree. # radial sampling for g1 and g2 -reduced shear -> ellipticiy and orientation A = random.uniform( 0, 0.67) # gal_q =b/a will ranges in (0.2,1) & A=1-q / 1+q gal_q = (1 - A) / (1 + A) gal_beta = random.uniform(0, 3.14) # radians g_1 = A * np.cos(2 * gal_beta) g_2 = A * np.sin(2 * gal_beta) gal_flux = 1e5 * random.uniform(0.3, 4) gal = galsim.Sersic(bulge_n, half_light_radius=bulge_re) gal = gal.withFlux(gal_flux) gal = gal.shear(g1=g_1, g2=g_2) psf = galsim.Moffat(beta=psf_beta, flux=1.0, fwhm=psf_re) final = galsim.Convolve([psf, gal]) image = galsim.ImageF(image_size, image_size, scale=pixel_scale) final.drawImage(image=image) # signal to noise ratio snr = np.sqrt((image.array**2).sum()) / noise # After generating the data, preserve only that with SNR [10, 100] if 10 <= snr <= 100: break image_nonoise = copy.deepcopy(image.array) image.addNoise(galsim.PoissonNoise(rng, sky_level=0.0)) # noise map for bkgr gaussian noise image.addNoise(galsim.GaussianNoise(rng, sigma=noise)) # Optionally: generate a PSF image if i == 0 or random_psf: psf_image = galsim.ImageF(image_size, image_size, scale=pixel_scale) psf.drawImage(image=psf_image) data["psf_img"][i] = psf_image.array data["img"][i] = image.array # final noised image data["img_nonoise"][i] = image_nonoise # noiseless image data["gal_flux"][i] = gal_flux data["bulge_re"][i] = bulge_re data["bulge_n"][i] = bulge_n data["gal_q"][i] = gal_q data["gal_beta"][i] = gal_beta data["psf_r"][i] = psf_re data["snr"][i] = snr data["sigma"][i] = noise data["g_1"][i] = g_1 data["g_2"][i] = g_2
def DrawStamp(psf, gal, config, xsize, ysize, offset, method): """ Draw an image using the given psf and gal profiles (which may be None) using the FFT method for doing the convolution. @returns the resulting image. """ # Setup the object to draw: prof_list = [ prof for prof in (gal,psf) if prof is not None ] assert len(prof_list) > 0 # Should have already been checked. if len(prof_list) > 1: final = galsim.Convolve(prof_list) else: final = prof_list[0] # Setup the kwargs to pass to drawImage kwargs = {} if xsize: kwargs['image'] = galsim.ImageF(xsize, ysize) kwargs['offset'] = offset kwargs['method'] = method if 'image' in config and 'wmult' in config['image']: kwargs['wmult'] = galsim.config.ParseValue(config['image'], 'wmult', config, float)[0] kwargs['wcs'] = config['wcs'].local(image_pos = config['image_pos']) if method == 'phot': kwargs['rng'] = config['rng'] # Check validity of extra phot options: max_extra_noise = None if 'image' in config and 'n_photons' in config['image']: if method != 'phot': raise AttributeError('n_photons is invalid with method != phot') if 'max_extra_noise' in config['image']: import warnings warnings.warn( "Both 'max_extra_noise' and 'n_photons' are set in config['image'], "+ "ignoring 'max_extra_noise'.") kwargs['n_photons'] = galsim.config.ParseValue(config['image'], 'n_photons', config, int)[0] elif 'image' in config and 'max_extra_noise' in config['image']: if method != 'phot': raise AttributeError('max_extra_noise is invalid with method != phot') max_extra_noise = galsim.config.ParseValue( config['image'], 'max_extra_noise', config, float)[0] elif method == 'phot': max_extra_noise = 0.01 if 'image' in config and 'poisson_flux' in config['image']: if method != 'phot': raise AttributeError('poisson_flux is invalid with method != phot') kwargs['poisson_flux'] = galsim.config.ParseValue( config['image'], 'poisson_flux', config, bool)[0] if max_extra_noise is not None: if max_extra_noise < 0.: raise ValueError("image.max_extra_noise cannot be negative") if max_extra_noise > 0.: if 'image' in config and 'noise' in config['image']: noise_var = galsim.config.CalculateNoiseVar(config) else: raise AttributeError( "Need to specify noise level when using draw_method = phot") if noise_var < 0.: raise ValueError("noise_var calculated to be < 0.") max_extra_noise *= noise_var kwargs['max_extra_noise'] = max_extra_noise im = final.drawImage(**kwargs) im.setOrigin(config['image_origin']) # If the object has a noise attribute, then check if we need to do anything with it. current_var = 0. # Default if not overwritten if hasattr(final,'noise'): if 'noise' in config['image']: noise = config['image']['noise'] if 'whiten' in noise: if 'symmetrize' in noise: raise AttributeError('Only one of whiten or symmetrize is allowed') whiten, safe = galsim.config.ParseValue(noise, 'whiten', config, bool) current_var = final.noise.whitenImage(im) elif 'symmetrize' in noise: symmetrize, safe = galsim.config.ParseValue(noise, 'symmetrize', config, int) current_var = final.noise.symmetrizeImage(im, symmetrize) if (('gal' in config and 'signal_to_noise' in config['gal']) or ('gal' not in config and 'psf' in config and 'signal_to_noise' in config['psf'])): if method == 'phot': raise NotImplementedError( "signal_to_noise option not implemented for draw_method = phot") import math import numpy if 'gal' in config: root_key = 'gal' else: root_key = 'psf' if 'flux' in config[root_key]: raise AttributeError( 'Only one of signal_to_noise or flux may be specified for %s'%root_key) if 'image' in config and 'noise' in config['image']: noise_var = galsim.config.CalculateNoiseVar(config) else: raise AttributeError( "Need to specify noise level when using %s.signal_to_noise"%root_key) sn_target = galsim.config.ParseValue(config[root_key], 'signal_to_noise', config, float)[0] # Now determine what flux we need to get our desired S/N # There are lots of definitions of S/N, but here is the one used by Great08 # We use a weighted integral of the flux: # S = sum W(x,y) I(x,y) / sum W(x,y) # N^2 = Var(S) = sum W(x,y)^2 Var(I(x,y)) / (sum W(x,y))^2 # Now we assume that Var(I(x,y)) is dominated by the sky noise, so # Var(I(x,y)) = var # We also assume that we are using a matched filter for W, so W(x,y) = I(x,y). # Then a few things cancel and we find that # S/N = sqrt( sum I(x,y)^2 / var ) sn_meas = math.sqrt( numpy.sum(im.array**2) / noise_var ) # Now we rescale the flux to get our desired S/N flux = sn_target / sn_meas im *= flux if hasattr(final,'noise'): current_var *= flux**2 return im, current_var
def test_IPC_basic(): # Make an image with non-trivially interesting scale. g = galsim.Gaussian(sigma=3.7) im = g.drawImage(scale=0.25) im_save = im.copy() # Check for no IPC ipc_kernel = galsim.Image(3, 3) ipc_kernel.setValue(2, 2, 1.0) im_new = im.copy() im_new.applyIPC(IPC_kernel=ipc_kernel, edge_treatment='extend') np.testing.assert_array_equal( im_new.array, im.array, err_msg="Image is altered for no IPC with edge_treatment = 'extend'") im_new.applyIPC(IPC_kernel=ipc_kernel, edge_treatment='wrap') np.testing.assert_array_equal( im_new.array, im.array, err_msg="Image is altered for no IPC with edge_treatment = 'wrap'") im_new.applyIPC(IPC_kernel=ipc_kernel, edge_treatment='crop') np.testing.assert_array_equal( im_new.array, im.array, err_msg="Image is altered for no IPC with edge_treatment = 'crop'") assert_raises(ValueError, im_new.applyIPC, galsim.Image(2, 2, init_value=1)) assert_raises(ValueError, im_new.applyIPC, galsim.Image(3, 3, init_value=-1)) assert_raises(ValueError, im_new.applyIPC, ipc_kernel * -1) assert_raises(ValueError, im_new.applyIPC, ipc_kernel, edge_treatment='invalid') # Test with a scalar fill_value fill_value = np.pi # a non-trivial one im_new.applyIPC(IPC_kernel=ipc_kernel, edge_treatment='crop', fill_value=fill_value) #Input arrays and output arrays will differ at the edges for this option. np.testing.assert_array_equal( im_new.array[1:-1, 1:-1], im.array[1:-1, 1:-1], err_msg= "Image is altered for no IPC with edge_treatment = 'crop' and with a fill_value" ) # Check if the edges are filled with fill_value np.testing.assert_array_equal( im_new.array[0, :], fill_value, err_msg="Top edge is not filled with the correct value by applyIPC") np.testing.assert_array_equal( im_new.array[-1, :], fill_value, err_msg="Bottom edge is not filled with the correct value by applyIPC") np.testing.assert_array_equal( im_new.array[:, 0], fill_value, err_msg="Left edge is not filled with the correct value by applyIPC") np.testing.assert_array_equal( im_new.array[:, -1], fill_value, err_msg="Left edge is not filled with the correct value by applyIPC") # Testing for flux conservation np.random.seed(1234) ipc_kernel = galsim.Image(abs(np.random.randn(3, 3))) # a random kernel im_new = im.copy() # Set edges to zero since flux is not conserved at the edges otherwise im_new.array[0, :] = 0.0 im_new.array[-1, :] = 0.0 im_new.array[:, 0] = 0.0 im_new.array[:, -1] = 0.0 with assert_warns(galsim.GalSimWarning): # warn about the sum not being 1 im_new.applyIPC(IPC_kernel=ipc_kernel, edge_treatment='extend') np.testing.assert_almost_equal( im_new.array.sum(), im.array[1:-1, 1:-1].sum(), 4, err_msg= "Normalized IPC kernel does not conserve the total flux for 'extend' option." ) # With kernel_normalization = False, it won't warn, but it also won't conserve flux. im_new = im.copy() im_new.applyIPC(IPC_kernel=ipc_kernel, edge_treatment='extend', kernel_normalization=False) assert np.abs(im_new.array.sum() - im.array[1:-1, 1:-1].sum()) > 1.e-8 im_new = im.copy() ipc_kernel /= ipc_kernel.array.sum( ) # Explicitly normalizing also avoids warning. im_new.applyIPC(IPC_kernel=ipc_kernel, edge_treatment='wrap') np.testing.assert_almost_equal( im_new.array.sum(), im.array.sum(), 4, err_msg= "Normalized IPC kernel does not conserve the total flux for 'wrap' option." ) # Checking directionality ipc_kernel = galsim.Image(3, 3) ipc_kernel.setValue(2, 2, 0.875) ipc_kernel.setValue(2, 3, 0.125) # This kernel should correspond to each pixel getting contribution from the pixel above it. im1 = im.copy() im1.applyIPC(IPC_kernel=ipc_kernel, edge_treatment='crop') np.testing.assert_array_almost_equal( 0.875 * im.array[1:-1, 1:-1] + 0.125 * im.array[2:, 1:-1], im1.array[1:-1, 1:-1], 7, err_msg="Difference in directionality for up kernel in applyIPC") # Checking for one pixel in the central bulk np.testing.assert_almost_equal( im1(2, 2), 0.875 * im(2, 2) + 0.125 * im(2, 3), 7, err_msg="Direction is not as intended for up kernel in applyIPC") ipc_kernel = galsim.Image(3, 3) ipc_kernel.setValue(2, 2, 0.875) ipc_kernel.setValue(1, 2, 0.125) # This kernel should correspond to each pixel getting contribution from the pixel to its left. im1 = im.copy() im1.applyIPC(IPC_kernel=ipc_kernel, edge_treatment='crop') np.testing.assert_array_almost_equal( im1.array[1:-1, 1:-1], im1.array[1:-1, 1:-1], 7, err_msg="Difference in directionality for left kernel in applyIPC") # Checking for one pixel in the central bulk np.testing.assert_almost_equal( im1(2, 3), 0.875 * im(2, 3) + 0.125 * im(2, 2), 7, err_msg="Direction is not as intended for left kernel in applyIPC") # Check using GalSim's native Convolve routine for GSObjects for a realisitic kernel ipc_kernel = galsim.Image( np.array([[0.01, 0.1, 0.01], [0.1, 1.0, 0.1], [0.01, 0.1, 0.01]])) ipc_kernel /= ipc_kernel.array.sum() ipc_kernel_int = galsim.InterpolatedImage(ipc_kernel, x_interpolant='nearest', scale=im.scale) im1 = im.copy() im1.applyIPC(IPC_kernel=ipc_kernel, edge_treatment='crop', kernel_normalization=False) im2 = im.copy() im2_int = galsim.InterpolatedImage(im2, x_interpolant='nearest') ipc_kernel_int = galsim.InterpolatedImage(ipc_kernel, x_interpolant='nearest', scale=im.scale) im_int = galsim.Convolve(ipc_kernel_int, im2_int, real_space=False) im_int.drawImage(im2, method='no_pixel', scale=im.scale) np.testing.assert_array_almost_equal( im1.array, im2.array, 6, err_msg="Output of applyIPC does not match the output from Convolve") try: with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=RuntimeWarning) from scipy import signal print( "SciPy found installed. Checking IPC kernel convolution against SciPy's `convolve2d`" ) # SciPy is going to emit a warning that we don't want to worry about, so let's deliberately # ignore it by going into a `catch_warnings` context. with warnings.catch_warnings(): warnings.simplefilter("ignore") # Generate an arbitrary kernel np.random.seed(2345) ipc_kernel = galsim.Image(abs(np.random.randn(3, 3))) ipc_kernel /= ipc_kernel.array.sum() # Convolution requires the kernel to be flipped up-down and left-right. im_new = im.copy() im_new.applyIPC(IPC_kernel=ipc_kernel, edge_treatment='extend', kernel_normalization=False) np.testing.assert_array_almost_equal( im_new.array, signal.convolve2d(im.array, np.flipud(np.fliplr(ipc_kernel.array)), mode='same', boundary='fill'), 7, err_msg= "Image differs from SciPy's result using `mode='same'` and " "`boundary='fill`.") im_new = im.copy() im_new.applyIPC(IPC_kernel=ipc_kernel, edge_treatment='crop', kernel_normalization=False) np.testing.assert_array_almost_equal( im_new.array[1:-1, 1:-1], signal.convolve2d(im.array, np.fliplr(np.flipud(ipc_kernel.array)), mode='valid', boundary='fill'), 7, err_msg= "Image differs from SciPy's result using `mode=valid'` and " "`boundary='fill'`.") im_new = im.copy() im_new.applyIPC(IPC_kernel=ipc_kernel, edge_treatment='wrap', kernel_normalization=False) np.testing.assert_array_almost_equal( im_new.array, signal.convolve2d(im.array, np.fliplr(np.flipud(ipc_kernel.array)), mode='same', boundary='wrap'), 7, err_msg= "Image differs from SciPy's result using `mode=same'` and " "boundary='wrap'`.") except ImportError: # Skip without any warning if SciPy is not installed pass
def main(argv): """ About as simple as it gets: - Use a circular Gaussian profile for the galaxy. - Convolve it by a circular Gaussian PSF. - Add Gaussian noise to the image. """ # In non-script code, use getLogger(__name__) at module scope instead. logging.basicConfig(format="%(message)s", level=logging.INFO, stream=sys.stdout) logger = logging.getLogger("demo1") gal_flux = 1.e5 # total counts on the image gal_sigma = 2. # arcsec psf_sigma = 1. # arcsec pixel_scale = 0.2 # arcsec / pixel noise = 30. # standard deviation of the counts in each pixel logger.info('Starting demo script 1 using:') logger.info(' - circular Gaussian galaxy (flux = %.1e, sigma = %.1f),', gal_flux, gal_sigma) logger.info(' - circular Gaussian PSF (sigma = %.1f),', psf_sigma) logger.info(' - pixel scale = %.2f,', pixel_scale) logger.info(' - Gaussian noise (sigma = %.2f).', noise) # Define the galaxy profile gal = galsim.Gaussian(flux=gal_flux, sigma=gal_sigma) logger.debug('Made galaxy profile') # Define the PSF profile psf = galsim.Gaussian(flux=1., sigma=psf_sigma) # PSF flux should always = 1 logger.debug('Made PSF profile') # Final profile is the convolution of these # Can include any number of things in the list, all of which are convolved # together to make the final flux profile. final = galsim.Convolve([gal, psf]) logger.debug('Convolved components into final profile') # Draw the image with a particular pixel scale, given in arcsec/pixel. # The returned image has a member, added_flux, which is gives the total flux actually added to # the image. One could use this value to check if the image is large enough for some desired # accuracy level. Here, we just ignore it. image = final.drawImage(scale=pixel_scale) logger.debug('Made image of the profile: flux = %f, added_flux = %f', gal_flux, image.added_flux) # Add Gaussian noise to the image with specified sigma image.addNoise(galsim.GaussianNoise(sigma=noise)) logger.debug('Added Gaussian noise') # Write the image to a file if not os.path.isdir('output'): os.mkdir('output') file_name = os.path.join('output', 'demo1.fits') # Note: if the file already exists, this will overwrite it. image.write(file_name) logger.info('Wrote image to %r' % file_name) # using %r adds quotes around filename for us results = image.FindAdaptiveMom() 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( 'Expected values in the limit that pixel response and noise are negligible:' ) logger.info(' e1 = %.3f, e2 = %.3f, sigma = %.3f', 0.0, 0.0, math.sqrt(gal_sigma**2 + psf_sigma**2) / pixel_scale)
import ngmix import matplotlib.pyplot as plt import ipdb mcal_shear = 0.01 #true_shear = -0.02 true_shear = 0.05 # Set up realistic observation parameters random_seed = 6222019; ud = galsim.UniformDeviate(random_seed+1) ` gal_ideal = galsim.InclinedExponential(80*galsim.degrees,half_light_radius=1.,flux=7000).rotate(20*galsim.degrees) psf = galsim.Gaussian(fwhm=.5) gal_ideal_observed = galsim.Convolve([gal_ideal,psf]) gal_ideal_image = gal_ideal_observed.drawImage(scale=0.206) sky_image = galsim.ImageF(gal_ideal_image) sky_level = 106 sky_image.fill(sky_level) gal_ideal_image+= sky_image # add a flat sky noise to image psf_image = psf.drawImage(scale=0.206) psf_weight_image = np.ones_like(psf_image.array)*1e9 weight_image = np.ones_like(gal_ideal_image.array)*1e9 # Set up for metacalibration. jj_im = ngmix.jacobian.DiagonalJacobian(scale=0.206,x=gal_ideal_image.center.x,y=gal_ideal_image.center.y) jj_psf = ngmix.jacobian.DiagonalJacobian(scale=0.206,x=psf_image.center.x,y=psf_image.center.y) psf_obs = ngmix.Observation(psf_image.array,weight=psf_weight_image,jacobian=jj_psf)
def estimate(n, p, g1, g2, el, s, show_last_gal, plot_progression, update_freq, typ, pxl): shear = [] shear.append(g1) # lensing shear to apply shear.append(g2) stamp_xsize = p stamp_ysize = p random_seed = 4218409 pixel_scale = 1.0 sky_level = 1.e6 gal_sigma = s gal_flux = 1.0 ########### print "\nPixel width and height is", p print "Applied shear is", shear print "Galaxy sigma in pixels", s print "Ellipticity is", el print "Number of galaxies is", n, '\n' if pxl: print "Will convolve with pixel" else: print "Will not convolve with pixel" if typ == 'g': print "Galaxy type is gaussian" elif typ == 'e': print "Galaxy type is exponential" else: print "Galaxy type unrecognized. Sersic not implemented yet." #gi=(sqrt(1+el)-sqrt(1-el))/(sqrt(1+el)+sqrt(1-el)) #shear due to intrinsic ellipticity px = [] py = [] py2 = [] py3 = [] py4 = [] img = galsim.ImageD(stamp_xsize, stamp_ysize, scale=pixel_scale) first_in_pair = True storeXY(stamp_xsize, stamp_ysize) #storeXY(3,3) #print XX #print YY #print XY #start = timeit.default_timer() for i in range(0, n): ud = galsim.UniformDeviate(random_seed + i) if first_in_pair: theta = ud() * 2. * pi * galsim.radians first_in_pair = False else: theta += 90 * galsim.degrees first_in_pair = True if typ == 'g': profile = galsim.Gaussian(sigma=gal_sigma, flux=gal_flux) elif typ == 'e': profile = galsim.Exponential(scale_radius=gal_sigma / 2., flux=gal_flux) else: print "Unrecognized galaxy type" sys.exit(2) gal = profile.createSheared(e=el, beta=theta) #gal=profile g = sqrt(shear[0]**2. + shear[1]**2.) gal.applyLensing(g1=shear[0], g2=shear[1], mu=1. / (1. - 2. * (g**2.))) if pxl: pix = galsim.Pixel(pixel_scale) final = galsim.Convolve([gal, pix]) else: final = gal #psf = galsim.Gaussian(sigma=1.0) final.draw(img) #myimg=drawEllipse(shear[0],shear[1],stamp_xsize,stamp_ysize,s) infarr = img.array q = getQuad(infarr, pxl, *np.shape(infarr)) E = polE(q) if ((i % update_freq) is 0) and (i > 0): print "Done " + str(i) + " galaxies..." py.append(0.5 * E[0]) py2.append(0.5 * E[1]) if plot_progression: px.append(i + 1) py3.append(np.array(py).mean()) py4.append(np.array(py2).mean()) #stop = timeit.default_timer() #print "\nAverage time spent on one galaxy: ", (stop-start)/(i+1) if plot_progression: plt.clf() ax = plt.subplot(1, 1, 1) ax.set_xlabel("Number of galaxies") ax.set_ylabel("Inferred shear") ax.plot(px, py3, 'ro') ax.plot(px, py4, 'bx') ax.axhline(y=shear[0]) ax.axhline(y=shear[1]) print "\nApplied shear is " + str(shear) for i, pp in enumerate([py, py2]): ## we are doing this clever rotation ## so we need to reduce by 2 pp = [(pp[2 * c] + pp[2 * c + 1]) / 2.0 for c in range(len(pp) / 2)] p = np.array(pp) inf = p.mean() sh = shear[i] err = p.std() / sqrt(len(p)) print "Inferred shear" + str( i + 1) + " is", inf, "+/-", err, "off by ", ( inf - sh) * 100 / sh, " %", (inf - sh) / err, " sigma" if show_last_gal: plt.subplot(2, 2, 1) arr = img.array / img.array.sum() plt.imshow(arr, interpolation='nearest') plt.colorbar() #plt.subplot (2,2,2) #plt.imshow(myimg, interpolation='nearest') #plt.colorbar() #plt.subplot (2,2,3) #plt.imshow((arr-myimg)/(myimg+arr+1e-5), interpolation='nearest') #plt.imshow((arr-myimg)/(myimg+1e-5), interpolation='nearest') #plt.imshow(img.array-img2.array, interpolation='nearest') #plt.colorbar() if show_last_gal or plot_progression: plt.show()
def simReal(real_galaxy, target_PSF, target_pixel_scale, g1=0.0, g2=0.0, rotation_angle=None, rand_rotate=True, rng=None, target_flux=1000.0, image=None): """Function to simulate images (no added noise) from real galaxy training data. This function takes a RealGalaxy from some training set, and manipulates it as needed to simulate a (no-noise-added) image from some lower-resolution telescope. It thus requires a target PSF (which could be an image, or one of our base classes) that represents all PSF components including the pixel response, and a target pixel scale. The default rotation option is to impose a random rotation to make irrelevant any real shears in the galaxy training data (optionally, the RNG can be supplied). This default can be turned off by setting `rand_rotate = False` or by requesting a specific rotation angle using the `rotation_angle` keyword, in which case `rand_rotate` is ignored. Optionally, the user can specify a shear (default 0). Finally, they can specify a flux normalization for the final image, default 1000. @param real_galaxy The RealGalaxy object to use, not modified in generating the simulated image. @param target_PSF The target PSF, either one of our base classes or an Image. @param target_pixel_scale The pixel scale for the final image, in arcsec. @param g1 First component of shear to impose (components defined with respect to pixel coordinates), [default: 0] @param g2 Second component of shear to impose, [default: 0] @param rotation_angle Angle by which to rotate the galaxy (must be an Angle instance). [default: None] @param rand_rotate Should the galaxy be rotated by some random angle? [default: True; unless `rotation_angle` is set, then False] @param rng A BaseDeviate instance to use for the random selection or rotation angle. [default: None] @param target_flux The target flux in the output galaxy image, [default: 1000.] @param image As with the GSObject.drawImage() function, if an image is provided, then it will be used and returned. [default: None, which means an appropriately-sized image will be created.] @return a simulated galaxy image. """ # do some checking of arguments if not isinstance(real_galaxy, galsim.RealGalaxy): raise RuntimeError("Error: simReal requires a RealGalaxy!") if isinstance(target_PSF, galsim.Image): target_PSF = galsim.InterpolatedImage(target_PSF, scale=target_pixel_scale) if not isinstance(target_PSF, galsim.GSObject): raise RuntimeError("Error: target PSF is not an Image or GSObject!") if rotation_angle is not None and not isinstance(rotation_angle, galsim.Angle): raise RuntimeError( "Error: specified rotation angle is not an Angle instance!") if (target_pixel_scale < real_galaxy.pixel_scale): import warnings message = "Warning: requested pixel scale is higher resolution than original!" warnings.warn(message) import math # needed for pi, sqrt below g = math.sqrt(g1**2 + g2**2) if g > 1: raise RuntimeError("Error: requested shear is >1!") # make sure target PSF is normalized target_PSF = target_PSF.withFlux(1.0) # rotate if rotation_angle is not None: real_galaxy = real_galaxy.rotate(rotation_angle) elif rotation_angle is None and rand_rotate == True: if rng is None: uniform_deviate = galsim.UniformDeviate() elif isinstance(rng, galsim.BaseDeviate): uniform_deviate = galsim.UniformDeviate(rng) else: raise TypeError("The rng provided is not a BaseDeviate") rand_angle = galsim.Angle(math.pi * uniform_deviate(), galsim.radians) real_galaxy = real_galaxy.rotate(rand_angle) # set fluxes real_galaxy = real_galaxy.withFlux(target_flux) # shear if (g1 != 0.0 or g2 != 0.0): real_galaxy = real_galaxy.shear(g1=g1, g2=g2) # convolve, resample out_gal = galsim.Convolve([real_galaxy, target_PSF]) image = out_gal.drawImage(image=image, scale=target_pixel_scale, method='no_pixel') # return simulated image return image
def main(): start = timer() p = get_params() #can't output anything but noise_var sys.stderr.write("Parameters chosen for postage stamp sims: \n") sys.stderr.write("{}\n".format(p)) rng = galsim.UniformDeviate(p.random_seed) #where to save images target_file_name = "stamps_{0}_.fits".format(p.id) template_file_name = "template_stamps_{0}_.fits".format(p.id) if p.noise_suppression == False: gal_name = os.path.splitext(target_file_name) else: gal_name = os.path.splitext(template_file_name) #initial noise noise = galsim.GaussianNoise(rng) #create test image to determine noise level if p.noise_suppression == False: test_psf = galsim.Moffat(3.5, half_light_radius=p.psf_hlr * p.pixel_scale) test_psf = test_psf.shear(e2=p.psf_e2) test_bulge = galsim.DeVaucouleurs(half_light_radius=( (p.gal_hlr_max - p.gal_hlr_min) / 2. + p.gal_hlr_min) * p.pixel_scale) test_disk = galsim.Exponential(half_light_radius=( (p.gal_hlr_max - p.gal_hlr_min) / 2. + p.gal_hlr_min) * p.pixel_scale) test_gal = 0.5 * test_bulge + 0.5 * test_disk test_gal = test_gal.withFlux(p.gal_flux_min) test_image = galsim.ImageF(p.stamp_size, p.stamp_size, scale=p.pixel_scale) test_final = galsim.Convolve([test_psf, test_gal]) test_final.drawImage(test_image) noise_var = test_image.addNoiseSNR(noise, p.gal_snr_min, preserve_flux=True) print noise_var #final noise noise = galsim.GaussianNoise(rng, sigma=np.sqrt(noise_var)) #make galaxy image gal_image = galsim.ImageF(p.nx_tiles * p.stamp_size, p.ny_tiles * p.stamp_size, scale=p.pixel_scale) #make ellipticity distribution from sigma ellip_dist = BobsEDist(p.gal_ellip_sigma, rng) gal_images, psf_images, gal_files, psf_files = [], [], [], [] for ifile in range(p.n_files): final_gals, images = [], [] #all files have same psf psf = galsim.Moffat(3.5, half_light_radius=p.psf_hlr * p.pixel_scale) psf = psf.shear(e2=p.psf_e2) psf_image = galsim.ImageF(p.stamp_size, p.stamp_size, scale=p.pixel_scale) psf.drawImage(psf_image) for iy in range(p.ny_tiles): for ix in range(p.nx_tiles): b = galsim.BoundsI(ix * p.stamp_size + 1, (ix + 1) * p.stamp_size, iy * p.stamp_size + 1, (iy + 1) * p.stamp_size) sub_gal_image = gal_image[b] hlr = (rng() * (p.gal_hlr_max - p.gal_hlr_min) + p.gal_hlr_min) * p.pixel_scale flux = rng() * (p.gal_flux_max - p.gal_flux_min) + p.gal_flux_min bulge_frac = rng() e1, e2 = ellip_dist.sample() #galaxy = ExponentialDisk + DeVaucouleurs this_bulge = galsim.DeVaucouleurs(half_light_radius=hlr) this_disk = galsim.Exponential(half_light_radius=hlr) #same ellipticity this_bulge = this_bulge.shear(e1=e1, e2=e2) this_disk = this_disk.shear(e1=e1, e2=e2) #Apply a shift in disk center within one pixel shift_r = p.pixel_scale / 2. dx = shift_r * (rng() * 2. - 1.) dy = shift_r * (rng() * 2. - 1.) this_disk = this_disk.shift(dx, dy) #Apply a shift in bulge center within circle of radius hlr theta = rng() * 2. * np.pi r = rng() * hlr dx = r * np.cos(theta) dy = r * np.sin(theta) this_bulge = this_bulge.shift(dx, dy) this_gal = bulge_frac * this_bulge + (1 - bulge_frac) * this_disk this_gal = this_gal.withFlux(flux) #All galaxies have same applied shear this_gal = this_gal.shear(g1=p.g1, g2=p.g2) final = galsim.Convolve([psf, this_gal]) final.drawImage(sub_gal_image) if p.noise_suppression == False: sub_gal_image.addNoise(noise) gal_file = os.path.join( p.file_dir, ''.join([gal_name[0], str(ifile + 1).zfill(2), gal_name[1]])) gal_files.append(gal_file) psf_file = os.path.join( p.file_dir, ''.join([gal_name[0], str(ifile + 1).zfill(2), '.psf'])) psf_files.append(psf_file) gal_images.append(gal_image) psf_images.append(psf_image) filepool = mp.Pool(12) filepool.map(writeToFile, zip(gal_images, gal_files)) psfpool = mp.Pool(12) psfpool.map(writeToFile, zip(psf_images, psf_files)) end = timer() sys.stderr.write("Images created in {} sec\n".format(end - start))
def main(argv): """ Make images using constant PSF and variable shear: - The main image is 0.2 x 0.2 degrees. - Pixel scale is 0.2 arcsec, hence the image is 3600 x 3600 pixels. - Applied shear is from a cosmological power spectrum read in from file. - The PSF is a real one from SDSS, and corresponds to a convolution of atmospheric PSF, optical PSF, and pixel response, which has been sampled at pixel centers. We used a PSF from SDSS in order to have a PSF profile that could correspond to what you see with a real telescope. However, in order that the galaxy resolution not be too poor, we tell GalSim that the pixel scale for that PSF image is 0.2" rather than 0.396". We are simultaneously lying about the intrinsic size of the PSF and about the pixel scale when we do this. - The galaxy images include some initial correlated noise from the original HST observation. However, we whiten the noise of the final image so the final image has stationary Gaussian noise, rather than correlated noise. """ logging.basicConfig(format="%(message)s", level=logging.INFO, stream=sys.stdout) logger = logging.getLogger("demo11") # Define some parameters we'll use below. # Normally these would be read in from some parameter file. base_stamp_size = 32 # number of pixels in each dimension of galaxy images # This will be scaled up according to the dilation. # Hence the "base_" prefix. pixel_scale = 0.2 # arcsec/pixel image_size = 0.2 * galsim.degrees # size of big image in each dimension image_size = int( (image_size / galsim.arcsec) / pixel_scale) # convert to pixels image_size_arcsec = image_size * pixel_scale # size of big image in each dimension (arcsec) noise_variance = 1.e4 # ADU^2 nobj = 288 # number of galaxies in entire field # (This corresponds to 2 galaxies / arcmin^2) grid_spacing = 90.0 # The spacing between the samples for the power spectrum # realization (arcsec) gal_signal_to_noise = 100 # S/N of each galaxy # random_seed is used for both the power spectrum realization and the random properties # of the galaxies. random_seed = 24783923 file_name = os.path.join('output', 'tabulated_power_spectrum.fits.fz') logger.info('Starting demo script 11') # Read in galaxy catalog cat_file_name = 'real_galaxy_catalog_example.fits' dir = 'data' real_galaxy_catalog = galsim.RealGalaxyCatalog(cat_file_name, dir=dir) logger.info('Read in %d real galaxies from catalog', real_galaxy_catalog.nobjects) # List of IDs to use. We select 5 particularly irregular galaxies for this demo. # Then we'll choose randomly from this list. id_list = [106416, 106731, 108402, 116045, 116448] # We will cache the galaxies that we make in order to save some of the calculations that # happen on construction. In particular, we don't want to recalculate the Fourier transforms # of the real galaxy images, so it's more efficient so make a store of RealGalaxy instances. # We start with them all = None, and fill them in as we make them. gal_list = [None] * len(id_list) # Setup the PowerSpectrum object we'll be using: # To do this, we first have to read in the tabulated shear power spectrum, often denoted # C_ell(ell), where ell has units of inverse angle and C_ell has units of angle^2. However, # GalSim works in the flat-sky approximation, so we use this notation interchangeably with # P(k). GalSim does not calculate shear power spectra for users, who must be able to provide # their own (or use the examples in the repository). # # Here we use a tabulated power spectrum from iCosmo (http://icosmo.org), with the following # cosmological parameters and survey design: # H_0 = 70 km/s/Mpc # Omega_m = 0.25 # Omega_Lambda = 0.75 # w_0 = -1.0 # w_a = 0.0 # n_s = 0.96 # sigma_8 = 0.8 # Smith et al. prescription for the non-linear power spectrum. # Eisenstein & Hu transfer function with wiggles. # Default dN/dz with z_med = 1.0 # The file has, as required, just two columns which are k and P(k). However, iCosmo works in # terms of ell and C_ell; ell is inverse radians and C_ell in radians^2. Since GalSim tends to # work in terms of arcsec, we have to tell it that the inputs are radians^-1 so it can convert # to store in terms of arcsec^-1. pk_file = os.path.join('data', 'cosmo-fid.zmed1.00.out') ps = galsim.PowerSpectrum(pk_file, units=galsim.radians) # The argument here is "e_power_function" which defines the E-mode power to use. logger.info('Set up power spectrum from tabulated P(k)') # Now let's read in the PSF. It's a real SDSS PSF, which means pixel scale of 0.396". However, # the typical seeing is 1.2" and we want to simulate better seeing, so we will just tell GalSim # that the pixel scale is 0.2". We have to be careful with SDSS PSF images, as they have an # added 'soft bias' of 1000 which has been removed before creation of this file, so that the sky # level is properly zero. Also, the file is bzipped, to demonstrate the new capability of # reading in a file that has been compressed in various ways (which GalSim can infer from the # filename). We want to read the image directly into an InterpolatedImage GSObject, so we can # manipulate it as needed (here, the only manipulation needed is convolution). We want a PSF # with flux 1, and we can set the pixel scale using a keyword. psf_file = os.path.join('data', 'example_sdss_psf_sky0.fits.bz2') psf = galsim.InterpolatedImage(psf_file, scale=pixel_scale, flux=1.) logger.info('Read in PSF image from bzipped FITS file') # Setup the image: full_image = galsim.ImageF(image_size, image_size) # The default convention for indexing an image is to follow the FITS standard where the # lower-left pixel is called (1,1). However, this can be counter-intuitive to people more # used to C or python indexing, where indices start at 0. It is possible to change the # coordinates of the lower-left pixel with the methods `setOrigin`. For this demo, we # switch to 0-based indexing, so the lower-left pixel will be called (0,0). full_image.setOrigin(0, 0) # As for demo10, we use random_seed+nobj for the random numbers required for the # whole image. In this case, both the power spectrum realization and the noise on the # full image we apply later. rng = galsim.BaseDeviate(random_seed + nobj) # We want to make random positions within our image. However, currently for shears from a power # spectrum we first have to get shears on a grid of positions, and then we can choose random # positions within that. So, let's make the grid. We're going to make it as large as the # image, with grid points spaced by 90 arcsec (hence interpolation only happens below 90" # scales, below the interesting scales on which we want the shear power spectrum to be # represented exactly). The lensing engine wants positions in arcsec, so calculate that: ps.buildGrid(grid_spacing=grid_spacing, ngrid=int(math.ceil(image_size_arcsec / grid_spacing)), rng=rng.duplicate()) logger.info('Made gridded shears') # We keep track of how much noise is already in the image from the RealGalaxies. # The default initial value is all pixels = 0. noise_image = galsim.ImageF(image_size, image_size) noise_image.setOrigin(0, 0) # Make a slightly non-trivial WCS. We'll use a slightly rotated coordinate system # and center it at the image center. theta = 0.17 * galsim.degrees # ( dudx dudy ) = ( cos(theta) -sin(theta) ) * pixel_scale # ( dvdx dvdy ) ( sin(theta) cos(theta) ) dudx = math.cos(theta.rad()) * pixel_scale dudy = -math.sin(theta.rad()) * pixel_scale dvdx = math.sin(theta.rad()) * pixel_scale dvdy = math.cos(theta.rad()) * pixel_scale image_center = full_image.trueCenter() affine = galsim.AffineTransform(dudx, dudy, dvdx, dvdy, origin=full_image.trueCenter()) # We can also put it on the celestial sphere to give it a bit more realism. # The TAN projection takes a (u,v) coordinate system on a tangent plane and projects # that plane onto the sky using a given point as the tangent point. The tangent # point should be given as a CelestialCoord. sky_center = galsim.CelestialCoord(ra=19.3 * galsim.hours, dec=-33.1 * galsim.degrees) # The third parameter, units, defaults to arcsec, but we make it explicit here. # It sets the angular units of the (u,v) intermediate coordinate system. wcs = galsim.TanWCS(affine, sky_center, units=galsim.arcsec) full_image.wcs = wcs # Now we need to loop over our objects: for k in range(nobj): time1 = time.time() # The usual random number generator using a different seed for each galaxy. ud = galsim.UniformDeviate(random_seed + k) # Draw the size from a plausible size distribution: N(r) ~ r^-2.5 # For this, we use the class DistDeviate which can draw deviates from an arbitrary # probability distribution. This distribution can be defined either as a functional # form as we do here, or as tabulated lists of x and p values, from which the # function is interpolated. # N.B. This calculation logically belongs later in the script, but given how the config # structure works and the fact that we also use this value for the stamp size # calculation, in order to get the output file to match the YAML output file, it # turns out this is where we need to put this use of the random number generator. distdev = galsim.DistDeviate(ud, function=lambda x: x**-2.5, x_min=1, x_max=5) dilat = distdev() # Choose a random position in the image x = ud() * (image_size - 1) y = ud() * (image_size - 1) image_pos = galsim.PositionD(x, y) # Turn this into a position in world coordinates # We leave this in the (u,v) plane, since the PowerSpectrum class is really defined # on the tangent plane, not in (ra,dec). world_pos = affine.toWorld(image_pos) # Get the reduced shears and magnification at this point g1, g2, mu = ps.getLensing(pos=world_pos) # Construct the galaxy: # Select randomly from among our list of galaxies. index = int(ud() * len(gal_list)) gal = gal_list[index] # If we haven't made this galaxy yet, we need to do so. if gal is None: # When whitening the image, we need to make sure the original correlated noise is # present throughout the whole image, otherwise the whitening will do the wrong thing # to the parts of the image that don't include the original image. The RealGalaxy # stores the correct noise profile to use as the gal.noise attribute. This noise # profile is automatically updated as we shear, dilate, convolve, etc. But we need to # tell it how large to pad with this noise by hand. This is a bit complicated for the # code to figure out on its own, so we have to supply the size for noise padding # with the noise_pad_size parameter. # In this case, the postage stamp will be 32 pixels for the undilated galaxies. # We expand the postage stamp as we dilate the galaxies, so that factor doesn't # come into play here. The shear and magnification are not significant, but the # image can be rotated, which adds an extra factor of sqrt(2). So the net required # padded size is # noise_pad_size = 32 * sqrt(2) * 0.2 arcsec/pixel = 9.1 arcsec # We round this up to 10 to be safe. gal = galsim.RealGalaxy(real_galaxy_catalog, rng=ud, id=id_list[index], noise_pad_size=10) # Save it for next time we use this galaxy. gal_list[index] = gal # Apply the dilation we calculated above. gal = gal.dilate(dilat) # Apply a random rotation theta = ud() * 2.0 * numpy.pi * galsim.radians gal = gal.rotate(theta) # Apply the cosmological (reduced) shear and magnification at this position using a single # GSObject method. gal = gal.lens(g1, g2, mu) # Convolve with the PSF. final = galsim.Convolve(psf, gal) # Account for the fractional part of the position: ix = int(math.floor(x + 0.5)) iy = int(math.floor(y + 0.5)) offset = galsim.PositionD(x - ix, y - iy) # Draw it with our desired stamp size (scaled up by the dilation factor): # Note: We make the stamp size odd to make the above calculation of the offset easier. this_stamp_size = 2 * int(math.ceil(base_stamp_size * dilat / 2)) + 1 stamp = galsim.ImageF(this_stamp_size, this_stamp_size) # We use method='no_pixel' here because the SDSS PSF image that we are using includes the # pixel response already. final.drawImage(image=stamp, wcs=wcs.local(image_pos), offset=offset, method='no_pixel') # Now we can whiten or symmetrize the noise on the postage stamp. Galsim automatically # propagates the noise correctly from the initial RealGalaxy object through the applied # shear, distortion, rotation, and convolution into the final object's noise attribute. To # make the noise fully white, use the image.whitenNoise() method. The returned value is the # variance of the Gaussian noise that is present after the whitening process. # # However, this is often overkill for many applications. If it is acceptable to merely end # up with noise with some degree of symmetry (say 4-fold or 8-fold symmetry), then you can # instead have GalSim just add enough noise to make the resulting noise have this kind of # symmetry. Usually this requires adding significantly less additional noise, which means # you can have the resulting total variance be somewhat smaller. The returned variance # corresponds to the zero-lag value of the noise correlation function, which will still have # off-diagonal elements. We can do this step using the image.symmetrizeNoise() method. #new_variance = stamp.whitenNoise(final.noise) new_variance = stamp.symmetrizeNoise(final.noise, 8) # Rescale flux to get the S/N we want. We have to do that before we add it to the big # image, which might have another galaxy near that point (so our S/N calculation would # erroneously include the flux from the other object). # See demo5.py for the math behind this calculation. sn_meas = math.sqrt(numpy.sum(stamp.array**2) / noise_variance) flux_scaling = gal_signal_to_noise / sn_meas stamp *= flux_scaling # This also scales up the current variance by flux_scaling**2. new_variance *= flux_scaling**2 # Recenter the stamp at the desired position: stamp.setCenter(ix, iy) # Find the overlapping bounds: bounds = stamp.bounds & full_image.bounds full_image[bounds] += stamp[bounds] # We need to keep track of how much variance we have currently in the image, so when # we add more noise, we can omit what is already there. noise_image[bounds] += new_variance time2 = time.time() tot_time = time2 - time1 logger.info('Galaxy %d: position relative to center = %s, t=%f s', k, str(world_pos), tot_time) # We already have some noise in the image, but it isn't uniform. So the first thing to do is # to make the Gaussian noise uniform across the whole image. We have a special noise class # that can do this. VariableGaussianNoise takes an image of variance values and applies # Gaussian noise with the corresponding variance to each pixel. # So all we need to do is build an image with how much noise to add to each pixel to get us # up to the maximum value that we already have in the image. max_current_variance = numpy.max(noise_image.array) noise_image = max_current_variance - noise_image vn = galsim.VariableGaussianNoise(rng, noise_image) full_image.addNoise(vn) # Now max_current_variance is the noise level across the full image. We don't want to add that # twice, so subtract off this much from the intended noise that we want to end up in the image. noise_variance -= max_current_variance # Now add Gaussian noise with this variance to the final image. We have to do this step # at the end, rather than adding to individual postage stamps, in order to get the noise # level right in the overlap regions between postage stamps. noise = galsim.GaussianNoise(rng, sigma=math.sqrt(noise_variance)) full_image.addNoise(noise) logger.info('Added noise to final large image') # Now write the image to disk. It is automatically compressed with Rice compression, # since the filename we provide ends in .fz. full_image.write(file_name) logger.info('Wrote image to %r', file_name) # Compute some sky positions of some of the pixels to compare with the values of RA, Dec # that ds9 reports. ds9 always uses (1,1) for the lower left pixel, so the pixel coordinates # of these pixels are different by 1, but you can check that the RA and Dec values are # the same as what GalSim calculates. ra_str = sky_center.ra.hms() dec_str = sky_center.dec.dms() logger.info('Center of image is at RA %sh %sm %ss, DEC %sd %sm %ss', ra_str[0:3], ra_str[3:5], ra_str[5:], dec_str[0:3], dec_str[3:5], dec_str[5:]) for (x, y) in [(0, 0), (0, image_size - 1), (image_size - 1, 0), (image_size - 1, image_size - 1)]: world_pos = wcs.toWorld(galsim.PositionD(x, y)) ra_str = world_pos.ra.hms() dec_str = world_pos.dec.dms() logger.info('Pixel (%4d, %4d) is at RA %sh %sm %ss, DEC %sd %sm %ss', x, y, ra_str[0:3], ra_str[3:5], ra_str[5:], dec_str[0:3], dec_str[3:5], dec_str[5:]) logger.info( 'ds9 reports these pixels as (1,1), (1,3600), etc. with the same RA, Dec.' )
def chromatic_galaxy(theta): gal_type,half_radius,pixel_scale,skyvar,redshift,e1,e2,indx =theta # where to find and output data path,filename = os.path.split(__file__) datapath = os.path.abspath(os.path.join(path,"data/")) outpath = os.path.abspath(os.path.join(path,"output/")) # In non-script code, use getLogger(__name__) at module scope instead logging.basicConfig(format="%(message)s",level=logging.INFO,stream=sys.stdout) logger = logging.getLogger("colored_images") # Initialize (pseudo-) random number generator random_seed = 1234567 rng = galsim.BaseDeviate(random_seed) # read in SEDs SED_names = ['CWW_E_ext','CWW_Sbc_ext','CWW_Scd_ext','CWW_Im_ext'] SEDs = {} for SED_name in SED_names: SED_filename = os.path.join(datapath,'{1}.sed'.format(SED_name)) SED = galsim.SED(SED_filename,wave_type='Ang') SEDs[SED_name] = SED.withFluxDensity(target_flux_density=1.0,wavelength=500) logger.debug('Successfully read in SEDs') filter_names = 'ugrizy' filters = {} for filter_name in filter_names: filter_filename = os.path.join(datapath,'LSST_{0}.dat'.format(filter_name)) filters[filter_name] = galsim.Bandpass(filter_filename) filters[filter_name] = filters[filter_name].thin(rel_err=1e-4) logger.debug('Read in filters') PSF = galsim.Moffat(fwhm=0.6,beta=2.5) #------------------------------------------------------------- # Part A: Chromatic de Vaucouleurs galaxy if gal_type == 'deVoucauleurs': logger.info('') logger.info('Starting part A: chromatic de Vaucouleurs galaxy') mono_gal = galsim.DeVaucouleurs(half_light_radius=half_radius) plt.imshow(mono_gal.drawImage(64,64).array) plt.show() SED = SEDs['CWW_E_ext'].atRedshift(redshift) gal = galsim.Chromatic(mono_gal,SED) gal = gal.shear(g1=0.5,g2=0.3).dilate(1.05).shift((1.0,2.1)) logger.debug('Created Chromatic') final = galsim.Convolve([gal,PSF]) logger.debug('Created final profile') gaussian_noise = galsim.GaussianNoise(rng,sigma=skyvar) for filter_name,filter_ in filters.iteritems(): img = galsim.ImageF(64,64,scale=pixel_scale) final.drawImage(filter_,image=img) #plt.imshow(final.drawImage(64,64).array) #plt.show() #img.addNoise(gaussian_noise) logger.debug('Created {0}-band image'.format(filter_name)) out_filename = os.path.join(outpath,'demo12a_{0}_{1}.fits'.format(filter_name,str(indx))) galsim.fits.write(img,out_filename) logger.debug('Wrote {0}-band image to disk'.format(filter_name)) logger.info('Added flux for {0}-band image:{1}'.format(filter_name,img.added_flux)) #----------------------------------------------------------------------- # PART B: chromatic bulge_disk galaxy if gal_type == 'diskbulge': logger.info('') logger.info('Starting part B: chromatic bulge_disk galaxy') mono_bulge = galsim.DeVaucouleurs(half_light_radius=0.05) bulge_SED = SEDs['CWW_E_ext'].atRedshift(redshift) bulge = mono_bulge * bulge_SED bulge = bulge.shear(g1=0.05,g2=0.07) logger.debug('Created bulge component') mono_disk = galsim.Exponential(half_light_radius=1.0) disk_SED = SEDs['CWW_Im_ext'].atRedshift(redshift) disk = mono_disk*disk_SED disk = disk.shear(g1=e1,g2=e2) logger.debug('Created disk component') bdgal = 1.1*(0.8*bulge+4.0*disk) bdfinal = galsim.Convolve([bdgal,PSF]) logger.debug('Created bulge+disk galaxy final profile') gaussian_noise = galsim.GaussianNoise(rng,sigma=skyvar) for filter_name,filter_ in filters.iteritems(): img = galsim.ImageF(64,64,scale=pixel_scale) bdfinal.drawImage(filter_,image=img) #img.addNoise(gaussian_noise) logger.debug('Created {0}-band image'.format(filter_name)) out_filename = os.path.join(outpath,'demo12b_{0}_{1}.fits'.format(filter_name,str(indx))) galsim.fits.write(img,out_filename) logger.debug('Wrote {0}-band image to disk'.format(filter_name)) logger.info('Added flux for {0}-band image:{1}'.format(filter_name,img.added_flux)) # PART C: chromatic real galaxy if gal_type == 'real': logger.info('') logger.info('Starting part B: chromatic bulge_disk galaxy') cubeimg = pf.getdata('cube_real.fits') idx = np.random.randint(low=0,high=99) imarr = cubeimg[idx,:,:] nx1,nx2 = np.shape(imarr) img = galsim.ImageF(nx1,nx2,scale=pixel_scale) bulge_SED = SEDs['CWW_E_ext'].atRedshift(redshift) bulge = mono_bulge * bulge_SED bulge = bulge.shear(g1=0.05,g2=0.07) logger.debug('Created bulge component') mono_disk = galsim.Exponential(half_light_radius=1.0) disk_SED = SEDs['CWW_Im_ext'].atRedshift(redshift) disk = mono_disk*disk_SED disk = disk.shear(g1=e1,g2=e2) logger.debug('Created disk component') bdgal = 1.1*(0.8*bulge+4.0*disk) bdfinal = galsim.Convolve([bdgal,PSF]) logger.debug('Created bulge+disk galaxy final profile') gaussian_noise = galsim.GaussianNoise(rng,sigma=skyvar) for filter_name,filter_ in filters.iteritems(): img = galsim.ImageF(64,64,scale=pixel_scale) bdfinal.drawImage(filter_,image=img) img.addNoise(gaussian_noise) logger.debug('Created {0}-band image'.format(filter_name)) out_filename = os.path.join(outpath,'demo12b_{0}_{1}.fits'.format(filter_name,str(indx))) galsim.fits.write(img,out_filename) logger.debug('Wrote {0}-band image to disk'.format(filter_name)) logger.info('Added flux for {0}-band image:{1}'.format(filter_name,img.added_flux))