def build_file(seed, file_name, mass, nobj, rng, truth_file_name, halo_id, first_obj_id): """A function that does all the work to build a single file. Returns the total time taken. """ t1 = time.time() # Build the image onto which we will draw the galaxies. full_image = galsim.ImageF(image_size, image_size) # The "true" center of the image is allowed to be halfway between two pixels, as is the # case for even-sized images. full_image.bounds.center() is an integer position, # which would be 1/2 pixel up and to the right of the true center in this case. im_center = full_image.bounds.trueCenter() # For the WCS, this time we use UVFunction, which lets you define arbitrary u(x,y) # and v(x,y) functions. We use a simple cubic radial function to create a # pincushion distortion. This is a typical kind of telescope distortion, although # we exaggerate the magnitude of the effect to make it more apparent. # The pixel size in the center of the image is 0.05, but near the corners (r=362), # the pixel size is approximately 0.075, which is much more distortion than is # normally present in typical telescopes. But it makes the effect of the variable # pixel area obvious when you look at the weight image in the output files. ufunc1 = lambda x, y: 0.05 * x * (1. + 2.e-6 * (x**2 + y**2)) vfunc1 = lambda x, y: 0.05 * y * (1. + 2.e-6 * (x**2 + y**2)) # It's not required to provide the inverse functions. However, if we don't, then # you will only be able to do toWorld operations, not the inverse toImage. # The inverse function does not have to be exact either. For example, you could provide # a function that does some kind of iterative solution to whatever accuracy you care # about. But in this case, we can do the exact inverse. # # Let w = sqrt(u**2 + v**2) and r = sqrt(x**2 + y**2). Then the solutions are: # x = (u/w) r and y = (u/w) r, and we use Cardano's method to solve for r given w: # See http://en.wikipedia.org/wiki/Cubic_function#Cardano.27s_method # # w = 0.05 r + 2.e-6 * 0.05 * r**3 # r = 100 * ( ( 5 sqrt(w**2 + 5.e3/27) + 5 w )**(1./3.) - # - ( 5 sqrt(w**2 + 5.e3/27) - 5 w )**(1./3.) ) def xfunc1(u, v): import math wsq = u * u + v * v if wsq == 0.: return 0. else: w = math.sqrt(wsq) temp = 5. * math.sqrt(wsq + 5.e3 / 27) r = 100. * ((temp + 5 * w)**(1. / 3.) - (temp - 5 * w)**(1. / 3)) return u * r / w def yfunc1(u, v): import math wsq = u * u + v * v if wsq == 0.: return 0. else: w = math.sqrt(wsq) temp = 5. * math.sqrt(wsq + 5.e3 / 27) r = 100. * ((temp + 5 * w)**(1. / 3.) - (temp - 5 * w)**(1. / 3)) return v * r / w # You could pass the above functions to UVFunction, and normally we would do that. # The only down side to doing so is that the specification of the WCS in the FITS # file is rather ugly. GalSim is able to turn the python byte code into strings, # but they are basically a really ugly mess of random-looking characters. GalSim # will be able to read it back in, but human readers will have no idea what WCS # function was used. To see what they look like, uncomment this line and comment # out the later wcs line. #wcs = galsim.UVFunction(ufunc1, vfunc1, xfunc1, yfunc1, origin=im_center) # If you provide the functions as strings, then those strings will be preserved # in the FITS header in a form that is more legible to human readers. # It also has the extra benefit of matching the output from demo9.yaml, which we # always try to do. The config file has no choice but to specify the functions # as strings. ufunc = '0.05 * x * (1. + 2.e-6 * (x**2 + y**2))' vfunc = '0.05 * y * (1. + 2.e-6 * (x**2 + y**2))' xfunc = ( '( lambda w: ( 0 if w==0 else ' + '100.*u/w*(( 5*(w**2 + 5.e3/27.)**0.5 + 5*w )**(1./3.) - ' + '( 5*(w**2 + 5.e3/27.)**0.5 - 5*w )**(1./3.))))( (u**2+v**2)**0.5 )' ) yfunc = ( '( lambda w: ( 0 if w==0 else ' + '100.*v/w*(( 5*(w**2 + 5.e3/27.)**0.5 + 5*w )**(1./3.) - ' + '( 5*(w**2 + 5.e3/27.)**0.5 - 5*w )**(1./3.))))( (u**2+v**2)**0.5 )' ) # The origin parameter defines where on the image should be considered (x,y) = (0,0) # in the WCS functions. wcs = galsim.UVFunction(ufunc, vfunc, xfunc, yfunc, origin=im_center) # Assign this wcs to full_image full_image.wcs = wcs # The weight image will hold the inverse variance for each pixel. # We can set the wcs directly on construction with the wcs parameter. weight_image = galsim.ImageF(image_size, image_size, wcs=wcs) # It is common for astrometric images to also have a bad pixel mask. We don't have any # defect simulation currently, so our bad pixel masks are currently all zeros. # But someday, we plan to add defect functionality to GalSim, at which point, we'll # be able to mark those defects on a bad pixel mask. # Note: the S in ImageS means to use "short int" for the data type. # This is a typical choice for a bad pixel image. badpix_image = galsim.ImageS(image_size, image_size, wcs=wcs) # We also draw a PSF image at the location of every galaxy. This isn't normally done, # and since some of the PSFs overlap, it's not necessarily so useful to have this kind # of image. But in this case, it's fun to look at the psf image, especially with # something like log scaling in ds9 to see how crazy an aberrated OpticalPSF with # struts can look when there is no atmospheric component to blur it out. psf_image = galsim.ImageF(image_size, image_size, wcs=wcs) # We will also write some truth information to an output catalog. # In real simulations, it is often useful to have a catalog of the truth values # to compare to measurements either directly or as cuts on the galaxy sample to # find where systematic errors are largest. # For now, we just make an empty OutputCatalog object with the names and types of the # columns. names = [ 'object_id', 'halo_id', 'flux', 'radius', 'h_over_r', 'inclination.rad', 'theta.rad', 'mu', 'redshift', 'shear.g1', 'shear.g2', 'pos.x', 'pos.y', 'image_pos.x', 'image_pos.y', 'halo_mass', 'halo_conc', 'halo_redshift' ] types = [ int, int, float, float, float, float, float, float, float, float, float, float, float, float, float, float, float, float ] truth_cat = galsim.OutputCatalog(names, types) # Setup the NFWHalo stuff: nfw = galsim.NFWHalo(mass=mass, conc=nfw_conc, redshift=nfw_z_halo, omega_m=omega_m, omega_lam=omega_lam) # Note: the last two are optional. If they are omitted, then (omega_m=0.3, omega_lam=0.7) # are actually the defaults. If you only specify one of them, the other is set so that # the total is 1. But you can define both values so that the total is not 1 if you want. # Radiation is assumed to be zero and dark energy equation of state w = -1. # If you want to include either radiation or more complicated dark energy models, # you can define your own cosmology class that defines the functions a(z), E(a), and # Da(z_source, z_lens). Then you can pass this to NFWHalo as a `cosmo` parameter. # Make the PSF profile outside the loop to minimize the (significant) OpticalPSF # construction overhead. psf = galsim.OpticalPSF(lam=psf_lam, diam=psf_D, obscuration=psf_obsc, nstruts=psf_nstruts, strut_thick=psf_strut_thick, strut_angle=psf_strut_angle, defocus=psf_defocus, astig1=psf_astig1, astig2=psf_astig2, coma1=psf_coma1, coma2=psf_coma2, trefoil1=psf_trefoil1, trefoil2=psf_trefoil2) for k in range(nobj): # Initialize the random number generator we will be using for this object: ud = galsim.UniformDeviate(seed + k + 1) # Determine where this object is going to go. # We choose points randomly within a donut centered at the center of the main image # in order to avoid placing galaxies too close to the halo center where the lensing # is not weak. We use an inner radius of 3 arcsec and an outer radius of 12 arcsec, # which takes us essentially to the edge of the image. radius = 12 inner_radius = 3 max_rsq = radius**2 min_rsq = inner_radius**2 while True: # (This is essentially a do..while loop.) x = (2. * ud() - 1) * radius y = (2. * ud() - 1) * radius rsq = x**2 + y**2 if rsq >= min_rsq and rsq <= max_rsq: break pos = galsim.PositionD(x, y) # We also need the position in pixels to determine where to place the postage # stamp on the full image. image_pos = wcs.toImage(pos) # For even-sized postage stamps, the nominal center (returned by stamp.bounds.center()) # cannot be at the true center (returned by stamp.bounds.trueCenter()) of the postage # stamp, since the nominal center values have to be integers. Thus, the nominal center # is 1/2 pixel up and to the right of the true center. # If we used odd-sized postage stamps, we wouldn't need to do this. x_nominal = image_pos.x + 0.5 y_nominal = image_pos.y + 0.5 # Get the integer values of these which will be the actual nominal center of the # postage stamp image. ix_nominal = int(math.floor(x_nominal + 0.5)) iy_nominal = int(math.floor(y_nominal + 0.5)) # The remainder will be accounted for in an offset when we draw. dx = x_nominal - ix_nominal dy = y_nominal - iy_nominal offset = galsim.PositionD(dx, dy) # Draw the flux from a power law distribution: N(f) ~ f^-1.5 # For this, we use the class DistDeviate which can draw deviates from an arbitrary # probability distribution. This distribution can be defined either as a functional # form as we do here, or as tabulated lists of x and p values, from which the # function is interpolated. flux_dist = galsim.DistDeviate(ud, function=lambda x: x**-1.5, x_min=gal_flux_min, x_max=gal_flux_max) flux = flux_dist() # We introduce here another surface brightness profile, called InclinedExponential. # It represents a typical 3D galaxy disk profile inclined at an arbitrary angle # relative to face on. # # inclination = 0 degrees corresponds to a face-on disk, which is equivalent to # the regular Exponential profile. # inclination = 90 degrees corresponds to an edge-on disk. # # A random orientation corresponds to the inclination angle taking the probability # distribution: # # P(inc) = 0.5 sin(inc) # # so we again use a DistDeviate to generate these values. inc_dist = galsim.DistDeviate(ud, function=lambda x: 0.5 * math.sin(x), x_min=0, x_max=math.pi) inclination = inc_dist() * galsim.radians # The parameters scale_radius and scale_height give the scale distances in the # 3D distribution: # # I(R,z) = I_0 / (2 scale_height) * sech^2(z/scale_height) * exp(-r/scale_radius) # # These values can be given separately if desired. However, it is often easier to # give the ratio scale_h_over_r as an independent value, since the radius and height # values are correlated, while h/r is approximately independent of h or r. h_over_r = ud() * (gal_h_over_r_max - gal_h_over_r_min) + gal_h_over_r_min radius = ud() * (gal_r_max - gal_r_min) + gal_r_min # The inclination is around the x-axis, so we want to rotate the galaxy by a # random angle. theta = ud() * math.pi * 2. * galsim.radians # Make the galaxy profile with these values: gal = galsim.InclinedExponential(scale_radius=radius, scale_h_over_r=h_over_r, inclination=inclination, flux=flux) gal = gal.rotate(theta) # Now apply the appropriate lensing effects for this position from # the NFW halo mass. try: g1, g2 = nfw.getShear(pos, nfw_z_source) nfw_shear = galsim.Shear(g1=g1, g2=g2) except: # This shouldn't happen, since we exclude the inner 10 arcsec, but it's a # good idea to use the try/except block here anyway. import warnings warnings.warn( "Warning: NFWHalo shear is invalid -- probably strong lensing! " + "Using shear = 0.") nfw_shear = galsim.Shear(g1=0, g2=0) nfw_mu = nfw.getMagnification(pos, nfw_z_source) if nfw_mu < 0: import warnings warnings.warn( "Warning: mu < 0 means strong lensing! Using mu=25.") nfw_mu = 25 elif nfw_mu > 25: import warnings warnings.warn( "Warning: mu > 25 means strong lensing! Using mu=25.") nfw_mu = 25 # Calculate the total shear to apply # Since shear addition is not commutative, it is worth pointing out that # the order is in the sense that the second shear is applied first, and then # the first shear. i.e. The field shear is taken to be behind the cluster. # Kind of a cosmic shear contribution between the source and the cluster. # However, this is not quite the same thing as doing: # gal.shear(field_shear).shear(nfw_shear) # since the shear addition ignores the rotation that would occur when doing the # above lines. This is normally ok, because the rotation is not observable, but # it is worth keeping in mind. total_shear = nfw_shear + field_shear # Apply the magnification and shear to the galaxy gal = gal.magnify(nfw_mu) gal = gal.shear(total_shear) # Build the final object final = galsim.Convolve([psf, gal]) # Draw the stamp image # To draw the image at a position other than the center of the image, you can # use the offset parameter, which applies an offset in pixels relative to the # center of the image. # We also need to provide the local wcs at the current position. local_wcs = wcs.local(image_pos) stamp = final.drawImage(wcs=local_wcs, offset=offset) # Recenter the stamp at the desired position: stamp.setCenter(ix_nominal, iy_nominal) # Find overlapping bounds bounds = stamp.bounds & full_image.bounds full_image[bounds] += stamp[bounds] # Also draw the PSF psf_stamp = galsim.ImageF( stamp.bounds) # Use same bounds as galaxy stamp psf.drawImage(psf_stamp, wcs=local_wcs, offset=offset) psf_image[bounds] += psf_stamp[bounds] # Add the truth information for this object to the truth catalog row = ((first_obj_id + k), halo_id, flux, radius, h_over_r, inclination.rad(), theta.rad(), nfw_mu, nfw_z_source, total_shear.g1, total_shear.g2, pos.x, pos.y, image_pos.x, image_pos.y, mass, nfw_conc, nfw_z_halo) truth_cat.addRow(row) # Add Poisson noise to the full image # Note: The normal calculation of Poission noise isn't quite correct right now. # The pixel area is variable, which means the amount of sky flux that enters each # pixel is also variable. The wcs classes have a function `makeSkyImage` which # will fill an image with the correct amount of sky flux given the sky level # in units of ADU/arcsec^2. We use the weight image as our work space for this. wcs.makeSkyImage(weight_image, sky_level) # Add this to the current full_image (temporarily). full_image += weight_image # Add Poisson noise, given the current full_image. # The config parser uses a different random number generator for file-level and # image-level values than for the individual objects. This makes it easier to # parallelize the calculation if desired. In fact, this is why we've been adding 1 # to each seed value all along. The seeds for the objects take the values # random_seed+1 .. random_seed+nobj. The seed for the image is just random_seed, # which we built already (below) when we calculated how many objects need to # be in each file. Use the same rng again here, since this is also at image scope. full_image.addNoise(galsim.PoissonNoise(rng)) # Subtract the sky back off. full_image -= weight_image # The weight image is nominally the inverse variance of the pixel noise. However, it is # common to exclude the Poisson noise from the objects themselves and only include the # noise from the sky photons. The variance of the noise is just the sky level, which is # what is currently in the weight_image. (If we wanted to include the variance from the # objects too, then we could use the full_image before we added the PoissonNoise to it.) # So all we need to do now is to invert the values in weight_image. weight_image.invertSelf() # Write the file to disk: galsim.fits.writeMulti( [full_image, badpix_image, weight_image, psf_image], file_name) # And write the truth catalog file truth_cat.write(truth_file_name) t2 = time.time() return t2 - t1
def test_poisson(): """Test the Poisson noise builder """ scale = 0.3 sky = 200 config = { 'image' : { 'type' : 'Single', 'random_seed' : 1234, 'pixel_scale' : scale, 'size' : 32, 'noise' : { 'type' : 'Poisson', 'sky_level' : sky, } }, 'gal' : { 'type' : 'Gaussian', 'sigma' : 1.1, 'flux' : 100, }, } # First build by hand rng = galsim.BaseDeviate(1234 + 1) gal = galsim.Gaussian(sigma=1.1, flux=100) im1a = gal.drawImage(nx=32, ny=32, scale=scale) sky_pixel = sky * scale**2 im1a.addNoise(galsim.PoissonNoise(rng, sky_level=sky_pixel)) # Compare to what config builds im1b = galsim.config.BuildImage(config) np.testing.assert_equal(im1b.array, im1a.array) # Check noise variance var1 = galsim.config.CalculateNoiseVariance(config) np.testing.assert_equal(var1, sky_pixel) var2 = galsim.Image(3,3) galsim.config.AddNoiseVariance(config, var2) np.testing.assert_almost_equal(var2.array, sky_pixel) # Check include_obj_var=True var3 = galsim.Image(32,32) galsim.config.AddNoiseVariance(config, var3, include_obj_var=True) np.testing.assert_almost_equal(var3.array, sky_pixel + im1a.array) # Repeat using photon shooting, which needs to do something slightly different, since the # signal photons already have shot noise. rng.seed(1234 + 1) im2a = gal.drawImage(nx=32, ny=32, scale=scale, method='phot', rng=rng) # Need to add Poisson noise for the sky, but not the signal (which already has shot noise) im2a.addNoise(galsim.DeviateNoise(galsim.PoissonDeviate(rng, mean=sky_pixel))) im2a -= sky_pixel # Compare to what config builds galsim.config.RemoveCurrent(config) config['image']['draw_method'] = 'phot' # Make sure it gets copied over to stamp properly. del config['stamp']['draw_method'] del config['_copied_image_keys_to_stamp'] im2b = galsim.config.BuildImage(config) np.testing.assert_equal(im2b.array, im2a.array) # Check non-trivial sky image galsim.config.RemoveCurrent(config) config['image']['sky_level'] = sky config['image']['wcs'] = { 'type' : 'UVFunction', 'ufunc' : '0.05*x + 0.001*x**2', 'vfunc' : '0.05*y + 0.001*y**2', } del config['image']['pixel_scale'] del config['wcs'] rng.seed(1234+1) wcs = galsim.UVFunction(ufunc='0.05*x + 0.001*x**2', vfunc='0.05*y + 0.001*y**2') im3a = gal.drawImage(nx=32, ny=32, wcs=wcs, method='phot', rng=rng) sky_im = galsim.Image(im3a.bounds, wcs=wcs) wcs.makeSkyImage(sky_im, sky) im3a += sky_im # Add 1 copy of the raw sky image for image[sky] noise_im = sky_im.copy() noise_im *= 2. # Now 2x because the noise includes both in image[sky] and noise[sky] noise_im.addNoise(galsim.PoissonNoise(rng)) noise_im -= 2.*sky_im im3a += noise_im im3b = galsim.config.BuildImage(config) np.testing.assert_almost_equal(im3b.array, im3a.array, decimal=6)
def test_whiten(): """Test the options in config to whiten images """ real_gal_dir = os.path.join('..','examples','data') real_gal_cat = 'real_galaxy_catalog_23.5_example.fits' config = { 'image' : { 'type' : 'Single', 'random_seed' : 1234, 'pixel_scale' : 0.05, }, 'stamp' : { 'type' : 'Basic', 'size' : 32, }, 'gal' : { 'type' : 'RealGalaxy', 'index' : 79, 'flux' : 100, }, 'psf' : { # This is really slow if we don't convolve by a PSF. 'type' : 'Gaussian', 'sigma' : 0.05 }, 'input' : { 'real_catalog' : { 'dir' : real_gal_dir , 'file_name' : real_gal_cat } } } # First build by hand (no whitening yet) rng = galsim.BaseDeviate(1234 + 1) rgc = galsim.RealGalaxyCatalog(os.path.join(real_gal_dir, real_gal_cat)) gal = galsim.RealGalaxy(rgc, index=79, flux=100, rng=rng) psf = galsim.Gaussian(sigma=0.05) final = galsim.Convolve(gal,psf) im1a = final.drawImage(nx=32, ny=32, scale=0.05) # Compare to what config builds galsim.config.ProcessInput(config) im1b, cv1b = galsim.config.BuildStamp(config, do_noise=False) np.testing.assert_equal(cv1b, 0.) np.testing.assert_equal(im1b.array, im1a.array) # Now add whitening, but no noise yet. cv1a = final.noise.whitenImage(im1a) print('From whiten, current_var = ',cv1a) galsim.config.RemoveCurrent(config) config['image']['noise'] = { 'whiten' : True, } im1c, cv1c = galsim.config.BuildStamp(config, do_noise=False) print('From BuildStamp, current_var = ',cv1c) np.testing.assert_equal(cv1c, cv1a) np.testing.assert_equal(im1c.array, im1a.array) rng1 = rng.duplicate() # Save current state of rng # 1. Gaussian noise ##### config['image']['noise'] = { 'type' : 'Gaussian', 'variance' : 50, 'whiten' : True, } galsim.config.RemoveCurrent(config) im2a = im1a.copy() im2a.addNoise(galsim.GaussianNoise(sigma=math.sqrt(50-cv1a), rng=rng)) im2b, cv2b = galsim.config.BuildStamp(config) np.testing.assert_almost_equal(cv2b, 50) np.testing.assert_almost_equal(im2b.array, im2a.array, decimal=5) # If whitening already added too much noise, raise an exception config['image']['noise']['variance'] = 1.e-5 try: np.testing.assert_raises(RuntimeError, galsim.config.BuildStamp,config) except ImportError: pass # 2. Poisson noise ##### config['image']['noise'] = { 'type' : 'Poisson', 'sky_level_pixel' : 50, 'whiten' : True, } galsim.config.RemoveCurrent(config) im3a = im1a.copy() sky = 50 - cv1a rng.reset(rng1.duplicate()) im3a.addNoise(galsim.PoissonNoise(sky_level=sky, rng=rng)) im3b, cv3b = galsim.config.BuildStamp(config) np.testing.assert_almost_equal(cv3b, 50, decimal=5) np.testing.assert_almost_equal(im3b.array, im3a.array, decimal=5) # It's more complicated if the sky is quoted per arcsec and the wcs is not uniform. config2 = galsim.config.CopyConfig(config) galsim.config.RemoveCurrent(config2) config2['image']['sky_level'] = 100 config2['image']['wcs'] = { 'type' : 'UVFunction', 'ufunc' : '0.05*x + 0.001*x**2', 'vfunc' : '0.05*y + 0.001*y**2', } del config2['image']['pixel_scale'] del config2['wcs'] config2['image']['noise']['symmetrize'] = 4 # Also switch to symmetrize, just to mix it up. del config2['image']['noise']['whiten'] rng.reset(1234+1) # Start fresh, since redoing the whitening/symmetrizing wcs = galsim.UVFunction(ufunc='0.05*x + 0.001*x**2', vfunc='0.05*y + 0.001*y**2') im3c = galsim.Image(32,32, wcs=wcs) im3c = final.drawImage(im3c) cv3c = final.noise.symmetrizeImage(im3c,4) sky = galsim.Image(im3c.bounds, wcs=wcs) wcs.makeSkyImage(sky, 100) mean_sky = np.mean(sky.array) im3c += sky extra_sky = 50 - cv3c im3c.addNoise(galsim.PoissonNoise(sky_level=extra_sky, rng=rng)) im3d, cv3d = galsim.config.BuildStamp(config2) np.testing.assert_almost_equal(cv3d, 50 + mean_sky, decimal=4) np.testing.assert_almost_equal(im3d.array, im3c.array, decimal=5) config['image']['noise']['sky_level_pixel'] = 1.e-5 try: np.testing.assert_raises(RuntimeError, galsim.config.BuildStamp,config) except ImportError: pass # 3. CCDNoise ##### config['image']['noise'] = { 'type' : 'CCD', 'sky_level_pixel' : 25, 'read_noise' : 5, 'gain' : 1, 'whiten' : True, } galsim.config.RemoveCurrent(config) im4a = im1a.copy() rn = math.sqrt(25-cv1a) rng.reset(rng1.duplicate()) im4a.addNoise(galsim.CCDNoise(sky_level=25, read_noise=rn, gain=1, rng=rng)) im4b, cv4b = galsim.config.BuildStamp(config) np.testing.assert_almost_equal(cv4b, 50, decimal=5) np.testing.assert_almost_equal(im4b.array, im4a.array, decimal=5) # Repeat with gain != 1 config['image']['noise']['gain'] = 3.7 galsim.config.RemoveCurrent(config) im5a = im1a.copy() rn = math.sqrt(25-cv1a * 3.7**2) rng.reset(rng1.duplicate()) im5a.addNoise(galsim.CCDNoise(sky_level=25, read_noise=rn, gain=3.7, rng=rng)) im5b, cv5b = galsim.config.BuildStamp(config) np.testing.assert_almost_equal(cv5b, 50, decimal=5) np.testing.assert_almost_equal(im5b.array, im5a.array, decimal=5) # And again with a non-trivial sky image galsim.config.RemoveCurrent(config2) config2['image']['noise'] = config['image']['noise'] config2['image']['noise']['symmetrize'] = 4 del config2['image']['noise']['whiten'] rng.reset(1234+1) im5c = galsim.Image(32,32, wcs=wcs) im5c = final.drawImage(im5c) cv5c = final.noise.symmetrizeImage(im5c, 4) sky = galsim.Image(im5c.bounds, wcs=wcs) wcs.makeSkyImage(sky, 100) mean_sky = np.mean(sky.array) im5c += sky rn = math.sqrt(25-cv5c * 3.7**2) im5c.addNoise(galsim.CCDNoise(sky_level=25, read_noise=rn, gain=3.7, rng=rng)) im5d, cv5d = galsim.config.BuildStamp(config2) np.testing.assert_almost_equal(cv5d, 50 + mean_sky, decimal=4) np.testing.assert_almost_equal(im5d.array, im5c.array, decimal=5) config['image']['noise']['sky_level_pixel'] = 1.e-5 config['image']['noise']['read_noise'] = 0 try: np.testing.assert_raises(RuntimeError, galsim.config.BuildStamp,config) except ImportError: pass # 4. COSMOSNoise ##### file_name = os.path.join(galsim.meta_data.share_dir,'acs_I_unrot_sci_20_cf.fits') config['image']['noise'] = { 'type' : 'COSMOS', 'file_name' : file_name, 'variance' : 50, 'whiten' : True, } galsim.config.RemoveCurrent(config) im6a = im1a.copy() rng.reset(rng1.duplicate()) noise = galsim.getCOSMOSNoise(file_name=file_name, variance=50, rng=rng) noise -= galsim.UncorrelatedNoise(cv1a, rng=rng, wcs=noise.wcs) im6a.addNoise(noise) im6b, cv6b = galsim.config.BuildStamp(config) np.testing.assert_almost_equal(cv6b, 50, decimal=5) np.testing.assert_almost_equal(im6b.array, im6a.array, decimal=5) config['image']['noise']['variance'] = 1.e-5 del config['_current_cn_tag'] try: np.testing.assert_raises(RuntimeError, galsim.config.BuildStamp,config) except ImportError: pass
def test_ccdnoise_phot(): """CCDNoise has some special code for photon shooting, so check that it works correctly. """ scale = 0.3 sky = 200 gain = 1.8 rn = 2.3 config = { 'image' : { 'type' : 'Single', 'random_seed' : 1234, 'pixel_scale' : scale, 'size' : 32, 'draw_method' : 'phot', 'noise' : { 'type' : 'CCD', 'gain' : gain, 'read_noise' : rn, 'sky_level' : sky, } }, 'gal' : { 'type' : 'Gaussian', 'sigma' : 1.1, 'flux' : 100, }, } # First build by hand rng = galsim.BaseDeviate(1234 + 1) gal = galsim.Gaussian(sigma=1.1, flux=100) im1a = gal.drawImage(nx=32, ny=32, scale=scale, method='phot', rng=rng) sky_pixel = sky * scale**2 # Need to add Poisson noise for the sky, but not the signal (which already has shot noise) im1a *= gain im1a.addNoise(galsim.DeviateNoise(galsim.PoissonDeviate(rng, mean=sky_pixel * gain))) im1a /= gain im1a -= sky_pixel im1a.addNoise(galsim.GaussianNoise(rng, sigma=rn/gain)) # Compare to what config builds im1b = galsim.config.BuildImage(config) np.testing.assert_equal(im1b.array, im1a.array) # Check noise variance var = sky_pixel / gain + rn**2 / gain**2 var1 = galsim.config.CalculateNoiseVariance(config) np.testing.assert_equal(var1, var) var2 = galsim.Image(3,3) galsim.config.AddNoiseVariance(config, var2) np.testing.assert_almost_equal(var2.array, var) # Check include_obj_var=True var3 = galsim.Image(32,32) galsim.config.AddNoiseVariance(config, var3, include_obj_var=True) np.testing.assert_almost_equal(var3.array, var + im1a.array/gain) # Some slightly different code paths if rn = 0 or gain = 1: del config['image']['noise']['gain'] del config['image']['noise']['read_noise'] del config['image']['noise']['_get'] rng.seed(1234 + 1) im2a = gal.drawImage(nx=32, ny=32, scale=scale, method='phot', rng=rng) im2a.addNoise(galsim.DeviateNoise(galsim.PoissonDeviate(rng, mean=sky_pixel))) im2a -= sky_pixel im2b = galsim.config.BuildImage(config) np.testing.assert_equal(im2b.array, im2a.array) var5 = galsim.config.CalculateNoiseVariance(config) np.testing.assert_equal(var5, sky_pixel) var6 = galsim.Image(3,3) galsim.config.AddNoiseVariance(config, var6) np.testing.assert_almost_equal(var6.array, sky_pixel) var7 = galsim.Image(32,32) galsim.config.AddNoiseVariance(config, var7, include_obj_var=True) np.testing.assert_almost_equal(var7.array, sky_pixel + im2a.array) # Check non-trivial sky image galsim.config.RemoveCurrent(config) config['image']['sky_level'] = sky config['image']['wcs'] = { 'type' : 'UVFunction', 'ufunc' : '0.05*x + 0.001*x**2', 'vfunc' : '0.05*y + 0.001*y**2', } del config['image']['pixel_scale'] del config['wcs'] rng.seed(1234+1) wcs = galsim.UVFunction(ufunc='0.05*x + 0.001*x**2', vfunc='0.05*y + 0.001*y**2') im3a = gal.drawImage(nx=32, ny=32, wcs=wcs, method='phot', rng=rng) sky_im = galsim.Image(im3a.bounds, wcs=wcs) wcs.makeSkyImage(sky_im, sky) im3a += sky_im # Add 1 copy of the raw sky image for image[sky] noise_im = sky_im.copy() noise_im *= 2. # Now 2x because the noise includes both in image[sky] and noise[sky] noise_im.addNoise(galsim.PoissonNoise(rng)) noise_im -= 2.*sky_im im3a += noise_im im3b = galsim.config.BuildImage(config) np.testing.assert_almost_equal(im3b.array, im3a.array, decimal=6) # And again with the rn and gain put back in. galsim.config.RemoveCurrent(config) config['image']['noise']['gain'] = gain config['image']['noise']['read_noise'] = rn del config['image']['noise']['_get'] rng.seed(1234+1) im4a = gal.drawImage(nx=32, ny=32, wcs=wcs, method='phot', rng=rng) wcs.makeSkyImage(sky_im, sky) im4a += sky_im noise_im = sky_im.copy() noise_im *= 2. * gain noise_im.addNoise(galsim.PoissonNoise(rng)) noise_im /= gain noise_im -= 2. * sky_im im4a += noise_im im4a.addNoise(galsim.GaussianNoise(rng, sigma=rn/gain)) im4b = galsim.config.BuildImage(config) np.testing.assert_almost_equal(im4b.array, im4a.array, decimal=6)
def test_withOrigin(): from test_wcs import Cubic # First EuclideantWCS types: wcs_list = [ galsim.OffsetWCS(0.3, galsim.PositionD(1, 1), galsim.PositionD(10, 23)), galsim.OffsetShearWCS(0.23, galsim.Shear(g1=0.1, g2=0.3), galsim.PositionD(12, 43)), galsim.AffineTransform(0.01, 0.26, -0.26, 0.02, galsim.PositionD(12, 43)), galsim.UVFunction(ufunc=lambda x, y: 0.2 * x, vfunc=lambda x, y: 0.2 * y), galsim.UVFunction(ufunc=lambda x, y: 0.2 * x, vfunc=lambda x, y: 0.2 * y, xfunc=lambda u, v: u / scale, yfunc=lambda u, v: v / scale), galsim.UVFunction(ufunc='0.2*x + 0.03*y', vfunc='0.01*x + 0.2*y'), ] color = 0.3 for wcs in wcs_list: # Original version of the shiftOrigin tests in do_nonlocal_wcs using deprecated name. new_origin = galsim.PositionI(123, 321) wcs3 = check_dep(wcs.withOrigin, new_origin) assert wcs != wcs3, name + ' is not != wcs.withOrigin(pos)' wcs4 = wcs.local(wcs.origin, color=color) assert wcs != wcs4, name + ' is not != wcs.local()' assert wcs4 != wcs, name + ' is not != wcs.local() (reverse)' world_origin = wcs.toWorld(wcs.origin, color=color) if wcs.isUniform(): if wcs.world_origin == galsim.PositionD(0, 0): wcs2 = wcs.local(wcs.origin, color=color).withOrigin(wcs.origin) assert wcs == wcs2, name + ' is not equal after wcs.local().withOrigin(origin)' wcs2 = wcs.local(wcs.origin, color=color).withOrigin(wcs.origin, wcs.world_origin) assert wcs == wcs2, name + ' not equal after wcs.local().withOrigin(origin,world_origin)' world_pos1 = wcs.toWorld(galsim.PositionD(0, 0), color=color) wcs3 = check_dep(wcs.withOrigin, new_origin) world_pos2 = wcs3.toWorld(new_origin, color=color) np.testing.assert_almost_equal( world_pos2.x, world_pos1.x, 7, 'withOrigin(new_origin) returned wrong world position') np.testing.assert_almost_equal( world_pos2.y, world_pos1.y, 7, 'withOrigin(new_origin) returned wrong world position') new_world_origin = galsim.PositionD(5352.7, 9234.3) wcs5 = check_dep(wcs.withOrigin, new_origin, new_world_origin, color=color) world_pos3 = wcs5.toWorld(new_origin, color=color) np.testing.assert_almost_equal( world_pos3.x, new_world_origin.x, 7, 'withOrigin(new_origin, new_world_origin) returned wrong position') np.testing.assert_almost_equal( world_pos3.y, new_world_origin.y, 7, 'withOrigin(new_origin, new_world_origin) returned wrong position') # Now some CelestialWCS types cubic_u = Cubic(2.9e-5, 2000., 'u') cubic_v = Cubic(-3.7e-5, 2000., 'v') center = galsim.CelestialCoord(23 * galsim.degrees, -13 * galsim.degrees) radec = lambda x, y: center.deproject_rad( cubic_u(x, y) * 0.2, cubic_v(x, y) * 0.2, projection='lambert') wcs_list = [ galsim.RaDecFunction(radec), galsim.AstropyWCS('1904-66_TAN.fits', dir='fits_files'), galsim.GSFitsWCS('tpv.fits', dir='fits_files'), galsim.FitsWCS('sipsample.fits', dir='fits_files'), ] for wcs in wcs_list: # Original version of the shiftOrigin tests in do_celestial_wcs using deprecated name. new_origin = galsim.PositionI(123, 321) wcs3 = wcs.shiftOrigin(new_origin) assert wcs != wcs3, name + ' is not != wcs.shiftOrigin(pos)' wcs4 = wcs.local(wcs.origin) assert wcs != wcs4, name + ' is not != wcs.local()' assert wcs4 != wcs, name + ' is not != wcs.local() (reverse)' world_pos1 = wcs.toWorld(galsim.PositionD(0, 0)) wcs3 = wcs.shiftOrigin(new_origin) world_pos2 = wcs3.toWorld(new_origin) np.testing.assert_almost_equal( world_pos2.distanceTo(world_pos1) / galsim.arcsec, 0, 7, 'shiftOrigin(new_origin) returned wrong world position')
def test_poisson(): """Test the Poisson noise builder """ scale = 0.3 sky = 200 config = { 'image' : { 'type' : 'Single', 'random_seed' : 1234, 'pixel_scale' : scale, 'size' : 32, 'noise' : { 'type' : 'Poisson', 'sky_level' : sky, } }, 'gal' : { 'type' : 'Gaussian', 'sigma' : 1.1, 'flux' : 100, }, } # First build by hand rng = galsim.BaseDeviate(1234 + 1) gal = galsim.Gaussian(sigma=1.1, flux=100) im1a = gal.drawImage(nx=32, ny=32, scale=scale) sky_pixel = sky * scale**2 im1a.addNoise(galsim.PoissonNoise(rng, sky_level=sky_pixel)) # Compare to what config builds im1b = galsim.config.BuildImage(config) np.testing.assert_equal(im1b.array, im1a.array) # Check noise variance var1 = galsim.config.CalculateNoiseVariance(config) np.testing.assert_equal(var1, sky_pixel) var2 = galsim.Image(3,3) galsim.config.AddNoiseVariance(config, var2) np.testing.assert_almost_equal(var2.array, sky_pixel) # Check include_obj_var=True var3 = galsim.Image(32,32) galsim.config.AddNoiseVariance(config, var3, include_obj_var=True) np.testing.assert_almost_equal(var3.array, sky_pixel + im1a.array) # Repeat using photon shooting, which needs to do something slightly different, since the # signal photons already have shot noise. rng.seed(1234 + 1) im2a = gal.drawImage(nx=32, ny=32, scale=scale, method='phot', rng=rng) # Need to add Poisson noise for the sky, but not the signal (which already has shot noise) im2a.addNoise(galsim.DeviateNoise(galsim.PoissonDeviate(rng, mean=sky_pixel))) im2a -= sky_pixel # Compare to what config builds galsim.config.RemoveCurrent(config) config['image']['draw_method'] = 'phot' # Make sure it gets copied over to stamp properly. del config['stamp']['draw_method'] del config['stamp']['_done'] im2b = galsim.config.BuildImage(config) np.testing.assert_equal(im2b.array, im2a.array) # Check non-trivial sky image galsim.config.RemoveCurrent(config) config['image']['sky_level'] = sky config['image']['wcs'] = { 'type' : 'UVFunction', 'ufunc' : '0.05*x + 0.001*x**2', 'vfunc' : '0.05*y + 0.001*y**2', } del config['image']['pixel_scale'] del config['wcs'] rng.seed(1234+1) wcs = galsim.UVFunction(ufunc='0.05*x + 0.001*x**2', vfunc='0.05*y + 0.001*y**2') im3a = gal.drawImage(nx=32, ny=32, wcs=wcs, method='phot', rng=rng) sky_im = galsim.Image(im3a.bounds, wcs=wcs) wcs.makeSkyImage(sky_im, sky) im3a += sky_im # Add 1 copy of the raw sky image for image[sky] noise_im = sky_im.copy() noise_im *= 2. # Now 2x because the noise includes both in image[sky] and noise[sky] noise_im.addNoise(galsim.PoissonNoise(rng)) noise_im -= 2.*sky_im im3a += noise_im im3b = galsim.config.BuildImage(config) np.testing.assert_almost_equal(im3b.array, im3a.array, decimal=6) # With tree rings, the sky includes them as well. config['image']['sensor'] = { 'type' : 'Silicon', 'treering_func' : { 'type' : 'File', 'file_name' : 'tree_ring_lookup.dat', 'amplitude' : 0.5 }, 'treering_center' : { 'type' : 'XY', 'x' : 0, 'y' : -500 } } galsim.config.RemoveCurrent(config) config = galsim.config.CleanConfig(config) rng.seed(1234+1) trfunc = galsim.LookupTable.from_file('tree_ring_lookup.dat', amplitude=0.5) sensor = galsim.SiliconSensor(treering_func=trfunc, treering_center=galsim.PositionD(0,-500), rng=rng) im4a = gal.drawImage(nx=32, ny=32, wcs=wcs, method='phot', rng=rng, sensor=sensor) sky_im = galsim.Image(im3a.bounds, wcs=wcs) wcs.makeSkyImage(sky_im, sky) areas = sensor.calculate_pixel_areas(sky_im, use_flux=False) sky_im *= areas im4a += sky_im noise_im = sky_im.copy() noise_im *= 2. noise_im.addNoise(galsim.PoissonNoise(rng)) noise_im -= 2.*sky_im im4a += noise_im im4b = galsim.config.BuildImage(config) np.testing.assert_almost_equal(im4b.array, im4a.array, decimal=6) # Can't have both sky_level and sky_level_pixel config['image']['noise']['sky_level_pixel'] = 2000. with assert_raises(galsim.GalSimConfigError): galsim.config.BuildImage(config) # Must have a valid noise type del config['image']['noise']['sky_level_pixel'] config['image']['noise']['type'] = 'Invalid' with assert_raises(galsim.GalSimConfigError): galsim.config.BuildImage(config) # noise must be a dict config['image']['noise'] = 'Invalid' with assert_raises(galsim.GalSimConfigError): galsim.config.BuildImage(config) # Can't have signal_to_noise and flux config['image']['noise'] = { 'type' : 'Poisson', 'sky_level' : sky } config['gal']['signal_to_noise'] = 100 with assert_raises(galsim.GalSimConfigError): galsim.config.BuildImage(config) # This should work del config['gal']['flux'] galsim.config.BuildImage(config) # These now hit the errors in CalculateNoiseVariance rather than AddNoise config['image']['noise']['type'] = 'Invalid' with assert_raises(galsim.GalSimConfigError): galsim.config.BuildImage(config) config['image']['noise'] = 'Invalid' with assert_raises(galsim.GalSimConfigError): galsim.config.BuildImage(config) del config['image']['noise'] with assert_raises(galsim.GalSimConfigError): galsim.config.BuildImage(config) # If rather than signal_to_noise, we have an extra_weight output, then it hits # a different error. config['gal']['flux'] = 100 del config['gal']['signal_to_noise'] config['output'] = { 'weight' : {} } config['image']['noise'] = { 'type' : 'Poisson', 'sky_level' : sky } galsim.config.SetupExtraOutput(config) galsim.config.SetupConfigFileNum(config, 0, 0, 0) # This should work again. galsim.config.BuildImage(config) config['image']['noise']['type'] = 'Invalid' with assert_raises(galsim.GalSimConfigError): galsim.config.BuildImage(config) config['image']['noise'] = 'Invalid' with assert_raises(galsim.GalSimConfigError): galsim.config.BuildImage(config)