def _add_sky_background(self, image, uniform_deviate): sky_level_counts = self.sky_level_nmgy * self.image_parameters.counts_per_nmgy if self.include_noise: poisson_deviate = galsim.PoissonDeviate(uniform_deviate, mean=sky_level_counts) image.addNoise(galsim.DeviateNoise(poisson_deviate)) else: image.array[:] = image.array + sky_level_counts
def addNoise(self, config, base, im, rng, current_var, draw_method, logger): # Get how much extra sky to assume from the image.noise attribute. sky = GetSky(base['image'], base) extra_sky = GetSky(config, base) total_sky = sky + extra_sky # for the return value if isinstance(total_sky, galsim.Image): var = np.mean(total_sky.array) else: var = total_sky # (This could be zero, in which case we only add poisson noise for the object photons) # If we already have some variance in the image (from whitening), then we subtract this # much off of the sky level. It's not precisely accurate, since the existing variance is # Gaussian, rather than Poisson, but it's the best we can do. if current_var: logger.debug( 'image %d, obj %d: Target variance is %f, current variance is %f', base.get('image_num', 0), base.get('obj_num', 0), var, current_var) if isinstance(total_sky, galsim.Image): test = np.any(total_sky.image.array < current_var) else: test = (total_sky < current_var) if test: raise RuntimeError( "Whitening already added more noise than the requested Poisson noise." ) total_sky -= current_var extra_sky -= current_var # At this point, there is a slight difference between fft and phot. For photon shooting, # the galaxy already has Poisson noise, so we want to make sure not to add that again! if draw_method == 'phot': # Only add in the noise from the sky. if isinstance(total_sky, galsim.Image): noise_im = total_sky.copy() noise_im.addNoise(galsim.PoissonNoise(rng)) noise_im -= total_sky # total_sky should now have zero mean, but with the noise of the total sky level. im += noise_im else: im.addNoise( galsim.DeviateNoise( galsim.PoissonDeviate(rng, mean=total_sky))) # This deviate adds a noisy version of the sky, so need to subtract the mean # back off. im -= total_sky else: im += extra_sky # Do the normal PoissonNoise calculation. im.addNoise(galsim.PoissonNoise(rng)) im -= extra_sky logger.debug('image %d, obj %d: Added Poisson noise', base.get('image_num', 0), base.get('obj_num', 0)) return var
def allDetectorEffects(img, rng=None, exptime=None): """ This utility applies all sources of noise and detector effects for WFIRST that are implemented in GalSim. In terms of noise, this includes the Poisson noise due to the signal (sky + background), dark current, and read noise. The detector effects that are included are reciprocity failure, quantization, nonlinearity, and interpixel capacitance. It also includes the necessary factors of gain. In short, the user should be able to pass in an Image with all sources of signal (background plus astronomical objects), and the Image will be modified to include all subsequent steps in the image generation process for WFIRST that are implemented in GalSim. @param img The Image to be modified. @param rng An optional galsim.BaseDeviate to use for the addition of noise. If None, a new one will be initialized. [default: None] @param exptime The exposure time, in seconds. If None, then the WFIRST default exposure time will be used. [default: None] """ # Deal appropriately with passed-in RNG, exposure time. if rng is None: rng = galsim.BaseDeviate() elif not isinstance(rng, galsim.BaseDeviate): raise TypeError( "The rng provided to RealGalaxy constructor is not a BaseDeviate") if exptime is None: exptime = galsim.wfirst.exptime # Add Poisson noise. poisson_noise = galsim.PoissonNoise(rng) img.addNoise(poisson_noise) # Reciprocity failure (use WFIRST routine, with the supplied exposure time). addReciprocityFailure(img, exptime=exptime) # Quantize. img.quantize() # Dark current (use exposure time). dark_current = galsim.wfirst.dark_current * exptime dark_noise = galsim.DeviateNoise(galsim.PoissonDeviate(rng, dark_current)) img.addNoise(dark_noise) # Nonlinearity (use WFIRST routine). applyNonlinearity(img) # IPC (use WFIRST routine). applyIPC(img) # Read noise. read_noise = galsim.GaussianNoise(rng, sigma=galsim.wfirst.read_noise) img.addNoise(read_noise) # Gain. img /= galsim.wfirst.gain # Quantize. img.quantize()
def _add_sky_background(self, image, band_index, uniform_deviate): sky_level_nelec = ( self.band_sky_level_nmgy[band_index] * self.image_parameters.band_nelec_per_nmgy[band_index]) if self.include_noise: poisson_deviate = galsim.PoissonDeviate(uniform_deviate, mean=sky_level_nelec) image.addNoise(galsim.DeviateNoise(poisson_deviate)) else: image.array[:] = image.array + sky_level_nelec
def rand_arr(shape, deviate): """Function to make a 2d array of random deviates (of any sort). @param shape A list of length 2, indicating the desired 2d array dimensions @param deviate Any GalSim deviate (see random.py) such as UniformDeviate, GaussianDeviate, etc. to be used to generate random numbers @returns A Numpy array of the desired dimensions with random numbers generated using the supplied deviate. """ if len(shape) is not 2: raise ValueError("Can only make a 2d array from this function!") # note reversed indices due to Numpy vs. Image array indexing conventions! tmp_img = galsim.ImageD(shape[1], shape[0]) galsim.DeviateNoise(deviate).applyTo(tmp_img.view()) return tmp_img.array
def AddNoiseCCD(noise, config, draw_method, rng, im, weight_im, current_var, sky, logger): # This process goes a lot like the Poisson routine. There are just two differences. # The Poisson noise is in the electron, not ADU, and now we allow for a gain = e-/ADU, # so we need to account for that properly. And we also allow for an additional Gaussian # read noise. # Get how much extra sky to assume from the image.noise attribute. opt = {'gain': float, 'read_noise': float} # The noise sky_level is only required here if the image doesn't have any. if sky: opt['sky_level'] = float opt['sky_level_pixel'] = float single = [] else: single = [{'sky_level': float, 'sky_level_pixel': float}] params = galsim.config.GetAllParams(noise, 'noise', config, opt=opt, single=single, ignore=noise_ignore)[0] gain = params.get('gain', 1.0) read_noise = params.get('read_noise', 0.0) read_noise_var = read_noise**2 if 'sky_level' in params: if 'sky_level_pixel' in params: raise AttributeError( "Only one of sky_level and sky_level_pixel is allowed for " "noise.type = CCD") sky_level = params['sky_level'] if im.wcs.isUniform(): extra_sky = sky_level * im.wcs.pixelArea() elif 'image_pos' in config: extra_sky = sky_level * im.wcs.pixelArea(config['image_pos']) else: extra_sky = galsim.Image(im.bounds, wcs=im.wcs) im.wcs.makeSkyImage(extra_sky, sky_level) elif 'sky_level_pixel' in params: extra_sky = params['sky_level_pixel'] else: extra_sky = 0. # If we are saving the noise level in a weight image, do that now. if weight_im: # Check if a weight image should include the object variance. # Note: For the phot case, we don't actually have an exact value for the variance in each # pixel, but the drawn image before adding the Poisson noise is our best guess for the # variance from the object's flux, so if we want the object variance included, this is # still the best we can do. include_obj_var = False if ('output' in config and 'weight' in config['output'] and 'include_obj_var' in config['output']['weight']): include_obj_var = galsim.config.ParseValue( config['output']['weight'], 'include_obj_var', config, bool)[0] if include_obj_var: # The image right now has the object variance in each pixel. So before going on with # the noise, copy these over to the weight image. (We invert this later...) weight_im.copyFrom(im) # Account for the gain and read noise if gain != 1.0: import math weight_im /= math.sqrt(gain) if read_noise != 0.0: weight_im += read_noise_var else: # Otherwise, just add in the current sky noise: if sky or read_noise != 0.0: weight_im += sky / gain + read_noise_var # And add in the extra sky noise: if extra_sky: weight_im += extra_sky # If we already have some variance in the image (from whitening), then we try to subtract it # from the read noise if possible. If now, we subtract the rest off of the sky level. It's # not precisely accurate, since the existing variance is Gaussian, rather than Poisson, but # it's the best we can do. if current_var: if isinstance(sky, galsim.Image) or isinstance(extra_sky, galsim.Image): test = ((sky + extra_sky).image.array / gain + read_noise_var < current_var).any() else: test = (sky + extra_sky) / gain + read_noise_var < current_var if test: raise RuntimeError( "Whitening already added more noise than requested CCD noise.") if read_noise_var >= current_var: # First try to take away from the read_noise, since this one is actually Gaussian. import math read_noise_var -= current_var read_noise = math.sqrt(read_noise_var) else: # Take read_noise down to zero, since already have at least that much already. current_var -= read_noise_var read_noise = 0 read_noise_var = 0 # Take the rest away from the sky level extra_sky -= current_var * gain # At this point, there is a slight difference between fft and phot. For photon shooting, the # galaxy already has Poisson noise, so we want to make sure not to add that again! if draw_method == 'phot': # Add in the noise from the sky. if isinstance(sky, galsim.Image) or isinstance(extra_sky, galsim.Image): noise_im = sky + extra_sky if gain != 1.0: noise_im *= gain noise_im.addNoise(galsim.PoissonNoise(rng)) if gain != 1.0: noise_im /= gain if sky: noise_im -= sky if extra_sky: noise_im -= extra_sky # noise_im should now have zero mean, but with the noise of the total sky level. im += noise_im else: total_sky = sky + extra_sky if total_sky > 0.: if gain != 1.0: im *= gain im.addNoise( galsim.DeviateNoise( galsim.PoissonDeviate(rng, mean=total_sky * gain))) if gain != 1.0: im /= gain im -= total_sky # And add the read noise if read_noise != 0.: im.addNoise(galsim.GaussianNoise(rng, sigma=read_noise)) else: # Do the normal CCDNoise calculation. im += extra_sky im.addNoise(galsim.CCDNoise(rng, gain=gain, read_noise=read_noise)) im -= extra_sky if logger: logger.debug( 'image %d, obj %d: Added CCD noise with sky = %f, ' + 'gain = %f, read_noise = %f', config['image_num'], config['obj_num'], sky, gain, read_noise)
def AddNoisePoisson(noise, config, draw_method, rng, im, weight_im, current_var, sky, logger): # Get how much extra sky to assume from the image.noise attribute. if sky: opt = {'sky_level': float, 'sky_level_pixel': float} single = [] else: opt = {} single = [{'sky_level': float, 'sky_level_pixel': float}] params = galsim.config.GetAllParams(noise, 'noise', config, opt=opt, single=single, ignore=noise_ignore)[0] if 'sky_level' in params: if 'sky_level_pixel' in params: raise AttributeError( "Only one of sky_level and sky_level_pixel is allowed for " "noise.type = Poisson") sky_level = params['sky_level'] if im.wcs.isUniform(): extra_sky = sky_level * im.wcs.pixelArea() elif 'image_pos' in config: extra_sky = sky_level * im.wcs.pixelArea(config['image_pos']) else: extra_sky = galsim.Image(im.bounds, wcs=im.wcs) im.wcs.makeSkyImage(extra_sky, sky_level) elif 'sky_level_pixel' in params: extra_sky = params['sky_level_pixel'] else: extra_sky = 0. # If we are saving the noise level in a weight image, do that now. if weight_im: # Check if a weight image should include the object variance. # Note: For the phot case, we don't actually have an exact value for the variance in each # pixel, but the drawn image before adding the Poisson noise is our best guess for the # variance from the object's flux, so if we want the object variance included, this is # still the best we can do. include_obj_var = False if ('output' in config and 'weight' in config['output'] and 'include_obj_var' in config['output']['weight']): include_obj_var = galsim.config.ParseValue( config['output']['weight'], 'include_obj_var', config, bool)[0] if include_obj_var: # The image right now has the object variance in each pixel. So before going on with # the noise, copy these over to the weight image. (We invert this later...) weight_im.copyFrom(im) else: # Otherwise, just add in the current sky noise: if sky: weight_im += sky # And add in the extra sky noise: if extra_sky: weight_im += extra_sky # If we already have some variance in the image (from whitening), then we subtract this much # off of the sky level. It's not precisely accurate, since the existing variance is Gaussian, # rather than Poisson, but it's the best we can do. if current_var: if isinstance(sky, galsim.Image) or isinstance(extra_sky, galsim.Image): test = ((sky + extra_sky).image.array < current_var).any() else: test = (sky + extra_sky < current_var) if test: raise RuntimeError( "Whitening already added more noise than requested Poisson noise." ) extra_sky -= current_var # At this point, there is a slight difference between fft and phot. For photon shooting, the # galaxy already has Poisson noise, so we want to make sure not to add that again! if draw_method == 'phot': # Only add in the noise from the sky. if isinstance(sky, galsim.Image) or isinstance(extra_sky, galsim.Image): noise_im = sky + extra_sky noise_im.addNoise(galsim.PoissonNoise(rng)) if sky: noise_im -= sky if extra_sky: noise_im -= extra_sky # noise_im should now have zero mean, but with the noise of the total sky level. im += noise_im else: total_sky = sky + extra_sky if total_sky > 0.: im.addNoise( galsim.DeviateNoise( galsim.PoissonDeviate(rng, mean=total_sky))) # This deviate adds a noisy version of the sky, so need to subtract the mean back # off. im -= total_sky else: im += extra_sky # Do the normal PoissonNoise calculation. im.addNoise(galsim.PoissonNoise(rng)) im -= extra_sky if logger: logger.debug('image %d, obj %d: Added Poisson noise with sky = %f', config['image_num'], config['obj_num'], sky)
def main(argv): """ Make a fits image cube where each frame has two images of the same galaxy drawn with regular FFT convolution and with photon shooting. We do this for 5 different PSFs and 5 different galaxies, each with 4 different (random) fluxes, sizes, and shapes. """ logging.basicConfig(format="%(message)s", level=logging.INFO, stream=sys.stdout) logger = logging.getLogger("demo7") # To turn off logging: #logger.propagate = False # Define some parameters we'll use below. # Make output directory if not already present. if not os.path.isdir('output'): os.mkdir('output') file_name = os.path.join('output', 'cube_phot.fits.gz') random_seed = 553728 sky_level = 1.e4 # ADU / arcsec^2 pixel_scale = 0.28 # arcsec nx = 64 ny = 64 gal_flux_min = 1.e4 # Range for galaxy flux gal_flux_max = 1.e5 gal_hlr_min = 0.3 # arcsec gal_hlr_max = 1.3 # arcsec gal_e_min = 0. # Range for ellipticity gal_e_max = 0.8 psf_fwhm = 0.65 # arcsec # This script is set up as a comparison between using FFTs for doing the convolutions and # shooting photons. The two methods have trade-offs in speed and accuracy which vary # with the kind of profile being drawn and the S/N of the object, among other factors. # In addition, for each method, there are a number of parameters GalSim uses that control # aspects of the calculation that further affect the speed and accuracy. # # We encapsulate these parameters with an object called GSParams. The default values # are intended to be accurate enough for normal precision shear tests, without sacrificing # too much speed. # # Any PSF or galaxy object can be given a gsparams argument on construction that can # have different values to make the calculation more or less accurate (typically trading # off for speed or memory). # # In this script, we adjust some of the values slightly, just to show you how it works. # You could play around with these values and see what effect they have on the drawn images. # Usually, it requires a pretty drastic change in these parameters for you to be able to # notice the difference by eye. But subtle effects that may impact the shapes of galaxies # can happen well before then. # Type help(galsim.GSParams) for the complete list of parameters and more detailed # documentation, including the default values for each parameter. gsparams = galsim.GSParams( alias_threshold= 1.e-2, # maximum fractional flux that may be aliased around edge of FFT maxk_threshold= 2.e-3, # k-values less than this may be excluded off edge of FFT xvalue_accuracy= 1.e-4, # approximations in real space aim to be this accurate kvalue_accuracy= 1.e-4, # approximations in fourier space aim to be this accurate shoot_accuracy= 1.e-4, # approximations in photon shooting aim to be this accurate minimum_fft_size=64) # minimum size of ffts logger.info('Starting demo script 7') # Make the pixel: pix = galsim.Pixel(xw=pixel_scale) # Make the PSF profiles: psf1 = galsim.Gaussian(fwhm=psf_fwhm, gsparams=gsparams) psf2 = galsim.Moffat(fwhm=psf_fwhm, beta=2.4, gsparams=gsparams) psf3_inner = galsim.Gaussian(fwhm=psf_fwhm, flux=0.8, gsparams=gsparams) psf3_outer = galsim.Gaussian(fwhm=2 * psf_fwhm, flux=0.2, gsparams=gsparams) psf3 = psf3_inner + psf3_outer atmos = galsim.Gaussian(fwhm=psf_fwhm, gsparams=gsparams) optics = galsim.OpticalPSF(lam_over_diam=0.6 * psf_fwhm, obscuration=0.4, defocus=0.1, astig1=0.3, astig2=-0.2, coma1=0.2, coma2=0.1, spher=-0.3, gsparams=gsparams) psf4 = galsim.Convolve([atmos, optics ]) # Convolve inherits the gsparams from the first # item in the list. (Or you can supply a gsparams # argument explicitly if you want to override this.) atmos = galsim.Kolmogorov(fwhm=psf_fwhm, gsparams=gsparams) optics = galsim.Airy(lam_over_diam=0.3 * psf_fwhm, gsparams=gsparams) psf5 = galsim.Convolve([atmos, optics]) psfs = [psf1, psf2, psf3, psf4, psf5] psf_names = [ "Gaussian", "Moffat", "Double Gaussian", "OpticalPSF", "Kolmogorov * Airy" ] psf_times = [0, 0, 0, 0, 0] psf_fft_times = [0, 0, 0, 0, 0] psf_phot_times = [0, 0, 0, 0, 0] # Make the galaxy profiles: gal1 = galsim.Gaussian(half_light_radius=1, gsparams=gsparams) gal2 = galsim.Exponential(half_light_radius=1, gsparams=gsparams) gal3 = galsim.DeVaucouleurs(half_light_radius=1, gsparams=gsparams) gal4 = galsim.Sersic(half_light_radius=1, n=2.5, gsparams=gsparams) bulge = galsim.Sersic(half_light_radius=0.7, n=3.2, gsparams=gsparams) disk = galsim.Sersic(half_light_radius=1.2, n=1.5, gsparams=gsparams) gal5 = 0.4 * bulge + 0.6 * disk # Net half-light radius is only approximate for this one. gals = [gal1, gal2, gal3, gal4, gal5] gal_names = [ "Gaussian", "Exponential", "Devaucouleurs", "n=2.5 Sersic", "Bulge + Disk" ] gal_times = [0, 0, 0, 0, 0] gal_fft_times = [0, 0, 0, 0, 0] gal_phot_times = [0, 0, 0, 0, 0] # Other times to keep track of: setup_times = 0 fft_times = 0 phot_times = 0 noise_times = 0 # Loop over combinations of psf, gal, and make 4 random choices for flux, size, shape. all_images = [] k = 0 for ipsf in range(len(psfs)): psf = psfs[ipsf] psf_name = psf_names[ipsf] for igal in range(len(gals)): gal = gals[igal] gal_name = gal_names[igal] for i in range(4): logger.debug('Start work on image %d', i) t1 = time.time() # Initialize the random number generator we will be using. rng = galsim.UniformDeviate(random_seed + k) # Get a new copy, we'll want to keep the original unmodified. gal1 = gal.copy() # Generate random variates: flux = rng() * (gal_flux_max - gal_flux_min) + gal_flux_min gal1.setFlux(flux) hlr = rng() * (gal_hlr_max - gal_hlr_min) + gal_hlr_min gal1.applyDilation(hlr) beta_ellip = rng() * 2 * math.pi * galsim.radians ellip = rng() * (gal_e_max - gal_e_min) + gal_e_min gal_shape = galsim.Shear(e=ellip, beta=beta_ellip) gal1.applyShear(gal_shape) # Build the final object by convolving the galaxy, PSF and pixel response. final = galsim.Convolve([psf, pix, gal1]) # For photon shooting, need a version without the pixel (see below). final_nopix = galsim.Convolve([psf, gal1]) # Create the large, double width output image image = galsim.ImageF(2 * nx + 2, ny) # Rather than provide a dx= argument to the draw commands, we can also # set the pixel scale in the image itself with setScale. image.setScale(pixel_scale) # Assign the following two "ImageViews", fft_image and phot_image. # Using the syntax below, these are views into the larger image. # Changes/additions to the sub-images referenced by the views are automatically # reflected in the original image. fft_image = image[galsim.BoundsI(1, nx, 1, ny)] phot_image = image[galsim.BoundsI(nx + 3, 2 * nx + 2, 1, ny)] logger.debug( ' Read in training sample galaxy and PSF from file') t2 = time.time() # Draw the profile final.draw(fft_image) logger.debug( ' Drew fft image. Total drawn flux = %f. .flux = %f', fft_image.array.sum(), final.getFlux()) t3 = time.time() # Add Poisson noise sky_level_pixel = sky_level * pixel_scale**2 fft_image.addNoise( galsim.PoissonNoise(rng, sky_level=sky_level_pixel)) t4 = time.time() # The next two lines are just to get the output from this demo script # to match the output from the parsing of demo7.yaml. rng = galsim.UniformDeviate(random_seed + k) rng() rng() rng() rng() # Repeat for photon shooting image. # Photon shooting automatically convolves by the pixel, so we've made sure not # to include it in the profile! final_nopix.drawShoot(phot_image, max_extra_noise=sky_level_pixel / 100, rng=rng) t5 = time.time() # For photon shooting, galaxy already has Poisson noise, so we want to make # sure not to add that noise again! Thus, we just add sky noise, which # is Poisson with the mean = sky_level_pixel pd = galsim.PoissonDeviate(rng, mean=sky_level_pixel) # DeviateNoise just adds the action of the given deviate to every pixel. phot_image.addNoise(galsim.DeviateNoise(pd)) # For PoissonDeviate, the mean is not zero, so for a background-subtracted # image, we need to subtract the mean back off when we are done. phot_image -= sky_level_pixel logger.debug( ' Added Poisson noise. Image fluxes are now %f and %f', fft_image.array.sum(), phot_image.array.sum()) t6 = time.time() # Store that into the list of all images all_images += [image] k = k + 1 logger.info( '%d: %s * %s, flux = %.2e, hlr = %.2f, ellip = (%.2f,%.2f)', k, gal_name, psf_name, flux, hlr, gal_shape.getE1(), gal_shape.getE2()) logger.debug(' Times: %f, %f, %f, %f, %f', t2 - t1, t3 - t2, t4 - t3, t5 - t4, t6 - t5) psf_times[ipsf] += t6 - t1 psf_fft_times[ipsf] += t3 - t2 psf_phot_times[ipsf] += t5 - t4 gal_times[igal] += t6 - t1 gal_fft_times[igal] += t3 - t2 gal_phot_times[igal] += t5 - t4 setup_times += t2 - t1 fft_times += t3 - t2 phot_times += t5 - t4 noise_times += t4 - t3 + t6 - t5 logger.info('Done making images of galaxies') logger.info('') logger.info('Some timing statistics:') logger.info(' Total time for setup steps = %f', setup_times) logger.info(' Total time for regular fft drawing = %f', fft_times) logger.info(' Total time for photon shooting = %f', phot_times) logger.info(' Total time for adding noise = %f', noise_times) logger.info('') logger.info('Breakdown by PSF type:') for ipsf in range(len(psfs)): logger.info(' %s: Total time = %f (fft: %f, phot: %f)', psf_names[ipsf], psf_times[ipsf], psf_fft_times[ipsf], psf_phot_times[ipsf]) logger.info('') logger.info('Breakdown by Galaxy type:') for igal in range(len(gals)): logger.info(' %s: Total time = %f (fft: %f, phot: %f)', gal_names[igal], gal_times[igal], gal_fft_times[igal], gal_phot_times[igal]) logger.info('') # Now write the image to disk. # With any write command, you can optionally compress the file using several compression # schemes: # 'gzip' uses gzip on the full output file. # 'bzip2' uses bzip2 on the full output file. # 'rice' uses rice compression on the image, leaving the fits headers readable. # 'gzip_tile' uses gzip in tiles on the output image, leaving the fits headers readable. # 'hcompress' uses hcompress on the image, but it is only valid for 2-d data, so it # doesn't work for writeCube. # 'plio' uses plio on the image, but it is only valid for positive integer data. # Furthermore, the first three have standard filename extensions associated with them, # so if you don't specify a compression, but the filename ends with '.gz', '.bz2' or '.fz', # the corresponding compression will be selected automatically. # In other words, the `compression='gzip'` specification is actually optional here: galsim.fits.writeCube(all_images, file_name, compression='gzip') logger.info('Wrote fft image to fits data cube %r', file_name)
def __init__(self, real_galaxy_catalog, index=None, id=None, random=False, rng=None, x_interpolant=None, k_interpolant=None, flux=None, pad_factor=0, noise_pad=False, pad_image=None, use_cache=True, gsparams=None): import pyfits import numpy as np # Code block below will be for galaxy selection; not all are currently implemented. Each # option must return an index within the real_galaxy_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._get_index_for_id(id) elif random is 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 to RealGalaxy constructor is not a BaseDeviate" ) use_index = int(real_galaxy_catalog.nobjects * uniform_deviate()) else: raise AttributeError('No method specified for selecting a galaxy!') # read in the galaxy, PSF images; for now, rely on pyfits to make I/O errors. Should # consider exporting this code into fits.py in some function that takes a filename and HDU, # and returns an ImageView gal_image = real_galaxy_catalog.getGal(use_index) PSF_image = real_galaxy_catalog.getPSF(use_index) # choose proper interpolant if x_interpolant is None: lan5 = galsim.Lanczos(5, conserve_flux=True, tol=1.e-4) self.x_interpolant = galsim.InterpolantXY(lan5) else: self.x_interpolant = galsim.utilities.convert_interpolant_to_2d( x_interpolant) if k_interpolant is None: self.k_interpolant = galsim.InterpolantXY( galsim.Quintic(tol=1.e-4)) else: self.k_interpolant = galsim.utilities.convert_interpolant_to_2d( k_interpolant) # read in data about galaxy from FITS binary table; store as normal attributes of RealGalaxy # save any other relevant information as instance attributes self.catalog_file = real_galaxy_catalog.file_name self.index = use_index self.pixel_scale = float(real_galaxy_catalog.pixel_scale[use_index]) # handle padding by an image specify_size = False padded_size = gal_image.getPaddedSize(pad_factor) if pad_image is not None: specify_size = True if isinstance(pad_image, str): pad_image = galsim.fits.read(pad_image) if (not isinstance(pad_image, galsim.BaseImageF) and not isinstance(pad_image, galsim.BaseImageD)): raise ValueError( "Supplied pad_image is not one of the allowed types!") # If an image was supplied directly or from a file, check its size: # Cannot use if too small. # Use to define the final image size otherwise. deltax = ((1 + pad_image.getXMax() - pad_image.getXMin()) - (1 + gal_image.getXMax() - gal_image.getXMin())) deltay = ((1 + pad_image.getYMax() - pad_image.getYMin()) - (1 + gal_image.getYMax() - gal_image.getYMin())) if deltax < 0 or deltay < 0: raise RuntimeError("Image supplied for padding is too small!") if pad_factor != 1. and pad_factor != 0.: import warnings msg = "Warning: ignoring specified pad_factor because user also specified\n" msg += " an image to use directly for the padding." warnings.warn(msg) else: if isinstance(gal_image, galsim.BaseImageF): pad_image = galsim.ImageF(padded_size, padded_size) if isinstance(gal_image, galsim.BaseImageD): pad_image = galsim.ImageD(padded_size, padded_size) # Set up the GaussianDeviate if not provided one, or check that the user-provided one # is of a valid type. Note if random was selected, we can use that uniform_deviate safely. if random is True: gaussian_deviate = galsim.GaussianDeviate(uniform_deviate) else: if rng is None: gaussian_deviate = galsim.GaussianDeviate() elif isinstance(rng, galsim.BaseDeviate): # Even if it's already a GaussianDeviate, we still want to make a new Gaussian # deviate that would generate the same sequence, because later we change the sigma # and we don't want to change it for the original one that was passed in. So don't # distinguish between GaussianDeviate and the other BaseDeviates here. gaussian_deviate = galsim.GaussianDeviate(rng) else: raise TypeError( "rng provided to RealGalaxy constructor is not a BaseDeviate" ) # handle noise-padding options try: noise_pad = galsim.config.value._GetBoolValue(noise_pad, '') except: pass if noise_pad: self.pad_variance = float(real_galaxy_catalog.variance[use_index]) # Check, is it "True" or something else? If True, we use Gaussian uncorrelated noise # using the stored variance in the catalog. Otherwise, if it's a CorrelatedNoise we use # it directly; if it's an Image of some sort we use it to make a CorrelatedNoise; if # it's a string, we read in the image from file and make a CorrelatedNoise. if type(noise_pad) is not bool: if isinstance(noise_pad, galsim.correlatednoise._BaseCorrelatedNoise): cn = noise_pad.copy() if rng: # Let user supplied RNG take precedence over that in user CN cn.setRNG(gaussian_deviate) # This small patch may have different overall variance, so rescale while # preserving the correlation structure by default cn.setVariance(self.pad_variance) elif (isinstance(noise_pad, galsim.BaseImageF) or isinstance(noise_pad, galsim.BaseImageD)): cn = galsim.CorrelatedNoise(gaussian_deviate, noise_pad) elif use_cache and noise_pad in RealGalaxy._cache_noise_pad: cn = RealGalaxy._cache_noise_pad[noise_pad] # Make sure that we are using the desired RNG by resetting that in this cached # CorrelatedNoise instance if rng: cn.setRNG(gaussian_deviate) # This small patch may have different overall variance, so rescale while # preserving the correlation structure cn.setVariance(self.pad_variance) elif isinstance(noise_pad, str): tmp_img = galsim.fits.read(noise_pad) cn = galsim.CorrelatedNoise(gaussian_deviate, tmp_img) if use_cache: RealGalaxy._cache_noise_pad[noise_pad] = cn # This small patch may have different overall variance, so rescale while # preserving the correlation structure cn.setVariance(self.pad_variance) else: raise RuntimeError( "noise_pad must be either a bool, CorrelatedNoise, Image, " + "or a filename for reading in an Image") # Set the GaussianDeviate sigma gaussian_deviate.setSigma(np.sqrt(self.pad_variance)) # populate padding image with noise field if type(noise_pad) is bool: pad_image.addNoise(galsim.DeviateNoise(gaussian_deviate)) else: pad_image.addNoise(cn) else: self.pad_variance = 0. # Now we have to check: was the padding determined using pad_factor? Or by passing in an # image for padding? Treat these cases differently: # (1) If the former, then we can simply have the C++ handle the padding process. # (2) If the latter, then we have to do the padding ourselves, and pass the resulting image # to the C++ with pad_factor explicitly set to 1. if specify_size is False: # Make the SBInterpolatedImage out of the image. self.original_image = galsim.SBInterpolatedImage( gal_image, xInterp=self.x_interpolant, kInterp=self.k_interpolant, dx=self.pixel_scale, pad_factor=pad_factor, pad_image=pad_image, gsparams=gsparams) else: # Leave the original image as-is. Instead, we shift around the image to be used for # padding. Find out how much x and y margin there should be on lower end: x_marg = int(np.round(0.5 * deltax)) y_marg = int(np.round(0.5 * deltay)) # Now reset the pad_image to contain the original image in an even way pad_image = pad_image.view() pad_image.setScale(self.pixel_scale) pad_image.setOrigin(gal_image.getXMin() - x_marg, gal_image.getYMin() - y_marg) # Set the central values of pad_image to be equal to the input image pad_image[gal_image.bounds] = gal_image self.original_image = galsim.SBInterpolatedImage( pad_image, xInterp=self.x_interpolant, kInterp=self.k_interpolant, dx=self.pixel_scale, pad_factor=1., gsparams=gsparams) # also make the original PSF image, with far less fanfare: we don't need to pad with # anything interesting. self.original_PSF = galsim.SBInterpolatedImage( PSF_image, xInterp=self.x_interpolant, kInterp=self.k_interpolant, dx=self.pixel_scale, gsparams=gsparams) # recalculate Fourier-space attributes rather than using overly-conservative defaults self.original_image.calculateStepK() self.original_image.calculateMaxK() self.original_PSF.calculateStepK() self.original_PSF.calculateMaxK() if flux != None: self.original_image.setFlux(flux) self.original_image.__class__ = galsim.SBTransform # correctly reflect SBProfile change self.original_PSF.setFlux(1.0) self.original_PSF.__class__ = galsim.SBTransform # correctly reflect SBProfile change # Calculate the PSF "deconvolution" kernel psf_inv = galsim.SBDeconvolve(self.original_PSF, gsparams=gsparams) # Initialize the SBProfile attribute GSObject.__init__( self, galsim.SBConvolve([self.original_image, psf_inv], gsparams=gsparams))
def addNoise(self, config, base, im, rng, current_var, draw_method, logger): # This process goes a lot like the Poisson routine. There are just two differences. # First, the Poisson noise is in electrons, not ADU, and now we allow for a gain = e-/ADU, # so we need to account for that properly. Second, we also allow for an additional Gaussian # read noise. gain, read_noise, read_noise_var = self.getCCDNoiseParams(config, base) # Get how much extra sky to assume from the image.noise attribute. sky = GetSky(base['image'], base, logger) extra_sky = GetSky(config, base, logger) total_sky = sky + extra_sky # for the return value if isinstance(total_sky, galsim.Image): var = np.mean(total_sky.array) + read_noise_var else: var = total_sky + read_noise_var # If we already have some variance in the image (from whitening), then we try to subtract # it from the read noise if possible. If not, we subtract the rest off of the sky level. # It's not precisely accurate, since the existing variance is Gaussian, rather than # Poisson, but it's the best we can do. if current_var: logger.debug( 'image %d, obj %d: Target variance is %f, current variance is %f', base.get('image_num', 0), base.get('obj_num', 0), var, current_var) read_noise_var_adu = read_noise_var / gain**2 if isinstance(total_sky, galsim.Image): test = np.any( total_sky.array / gain + read_noise_var_adu < current_var) else: target_var = total_sky / gain + read_noise_var_adu logger.debug( 'image %d, obj %d: Target variance is %f, current variance is %f', base.get('image_num', 0), base.get('obj_num', 0), target_var, current_var) test = target_var < current_var if test: raise galsim.GalSimConfigError( "Whitening already added more noise than the requested CCD noise." ) if read_noise_var_adu >= current_var: # First try to take away from the read_noise, since this one is actually Gaussian. import math read_noise_var -= current_var * gain**2 read_noise = math.sqrt(read_noise_var) else: # Take read_noise down to zero, since already have at least that much already. current_var -= read_noise_var_adu read_noise = 0 read_noise_var = 0 # Take the rest away from the sky level total_sky -= current_var * gain extra_sky -= current_var * gain # At this point, there is a slight difference between fft and phot. For photon shooting, # the galaxy already has Poisson noise, so we want to make sure not to add that again! if draw_method == 'phot': # Add in the noise from the sky. if isinstance(total_sky, galsim.Image): noise_im = total_sky.copy() if gain != 1.0: noise_im *= gain noise_im.addNoise(galsim.PoissonNoise(rng)) if gain != 1.0: noise_im /= gain noise_im -= total_sky # total_sky should now have zero mean, but with the noise of the total sky level. im += noise_im else: if gain != 1.0: im *= gain pd = galsim.PoissonDeviate(rng, mean=total_sky * gain) im.addNoise(galsim.DeviateNoise(pd)) if gain != 1.0: im /= gain im -= total_sky # And add the read noise if read_noise != 0.: im.addNoise(galsim.GaussianNoise(rng, sigma=read_noise / gain)) else: # Do the normal CCDNoise calculation. if isinstance(total_sky, galsim.Image): im += extra_sky im.addNoise( galsim.CCDNoise(rng, gain=gain, read_noise=read_noise)) im -= extra_sky else: im.addNoise( galsim.CCDNoise(rng, gain=gain, read_noise=read_noise, sky_level=extra_sky)) logger.debug( 'image %d, obj %d: Added CCD noise with gain = %f, read_noise = %f', base.get('image_num', 0), base.get('obj_num', 0), gain, read_noise) return var
def __init__(self, image, x_interpolant = None, k_interpolant = None, normalization = 'flux', dx = None, flux = None, pad_factor = 0., noise_pad = 0., rng = None, pad_image = None, calculate_stepk=True, calculate_maxk=True, use_cache=True, use_true_center=True, gsparams=None): import numpy as np # first try to read the image as a file. If it's not either a string or a valid # pyfits hdu or hdulist, then an exception will be raised, which we ignore and move on. try: image = galsim.fits.read(image) except: pass # make sure image is really an image and has a float type if not isinstance(image, galsim.BaseImageF) and not isinstance(image, galsim.BaseImageD): raise ValueError("Supplied image is not an image of floats or doubles!") # it must have well-defined bounds, otherwise seg fault in SBInterpolatedImage constructor if not image.getBounds().isDefined(): raise ValueError("Supplied image does not have bounds defined!") # check what normalization was specified for the image: is it an image of surface # brightness, or flux? if not normalization.lower() in ("flux", "f", "surface brightness", "sb"): raise ValueError(("Invalid normalization requested: '%s'. Expecting one of 'flux', "+ "'f', 'surface brightness', or 'sb'.") % normalization) # set up the interpolants if none was provided by user, or check that the user-provided ones # are of a valid type if x_interpolant is None: self.x_interpolant = galsim.InterpolantXY(galsim.Quintic(tol=1e-4)) else: self.x_interpolant = galsim.utilities.convert_interpolant_to_2d(x_interpolant) if k_interpolant is None: self.k_interpolant = galsim.InterpolantXY(galsim.Quintic(tol=1e-4)) else: self.k_interpolant = galsim.utilities.convert_interpolant_to_2d(k_interpolant) # Check for input dx, and check whether Image already has one set. At the end of this # code block, either an exception will have been raised, or the input image will have a # valid scale set. if dx is None: dx = image.scale if dx == 0: raise ValueError("No information given with Image or keywords about pixel scale!") else: if type(dx) != float: dx = float(dx) # Don't change the original image. Make a new view if we need to set the scale. image = image.view() image.setScale(dx) if dx == 0.0: raise ValueError("dx may not be 0.0") # Set up the GaussianDeviate if not provided one, or check that the user-provided one is # of a valid type. if rng is None: gaussian_deviate = galsim.GaussianDeviate() elif isinstance(rng, galsim.BaseDeviate): # Even if it's already a GaussianDeviate, we still want to make a new Gaussian deviate # that would generate the same sequence, because later we change the sigma and we don't # want to change it for the original one that was passed in. So don't distinguish # between GaussianDeviate and the other BaseDeviates here. gaussian_deviate = galsim.GaussianDeviate(rng) else: raise TypeError("rng provided to InterpolatedImage constructor is not a BaseDeviate") # decide about deterministic image padding specify_size = False padded_size = image.getPaddedSize(pad_factor) if pad_image: specify_size = True if isinstance(pad_image, str): pad_image = galsim.fits.read(pad_image) if ( not isinstance(pad_image, galsim.BaseImageF) and not isinstance(pad_image, galsim.BaseImageD) ): raise ValueError("Supplied pad_image is not one of the allowed types!") # If an image was supplied directly or from a file, check its size: # Cannot use if too small. # Use to define the final image size otherwise. deltax = (1+pad_image.getXMax()-pad_image.getXMin())-(1+image.getXMax()-image.getXMin()) deltay = (1+pad_image.getYMax()-pad_image.getYMin())-(1+image.getYMax()-image.getYMin()) if deltax < 0 or deltay < 0: raise RuntimeError("Image supplied for padding is too small!") if pad_factor != 1. and pad_factor != 0.: import warnings msg = "Warning: ignoring specified pad_factor because user also specified\n" msg += " an image to use directly for the padding." warnings.warn(msg) elif noise_pad: if isinstance(image, galsim.BaseImageF): pad_image = galsim.ImageF(padded_size, padded_size) if isinstance(image, galsim.BaseImageD): pad_image = galsim.ImageD(padded_size, padded_size) # now decide about noise padding # First, see if the input is consistent with a float. # i.e. it could be an int, or a str that converts to a number. try: noise_pad = float(noise_pad) except: pass if isinstance(noise_pad, float): if noise_pad < 0.: raise ValueError("Noise variance cannot be negative!") elif noise_pad > 0.: # Note: make sure the sigma is properly set to sqrt(noise_pad). gaussian_deviate.setSigma(np.sqrt(noise_pad)) pad_image.addNoise(galsim.DeviateNoise(gaussian_deviate)) else: if isinstance(noise_pad, galsim.correlatednoise._BaseCorrelatedNoise): cn = noise_pad.copy() if rng: # Let a user supplied RNG take precedence over that in user CN cn.setRNG(gaussian_deviate) elif isinstance(noise_pad,galsim.BaseImageF) or isinstance(noise_pad,galsim.BaseImageD): cn = galsim.CorrelatedNoise(gaussian_deviate, noise_pad) elif use_cache and noise_pad in InterpolatedImage._cache_noise_pad: cn = InterpolatedImage._cache_noise_pad[noise_pad] if rng: # Make sure that we are using a specified RNG by resetting that in this cached # CorrelatedNoise instance, otherwise preserve the cached RNG cn.setRNG(gaussian_deviate) elif isinstance(noise_pad, str): cn = galsim.CorrelatedNoise(gaussian_deviate, galsim.fits.read(noise_pad)) if use_cache: InterpolatedImage._cache_noise_pad[noise_pad] = cn else: raise ValueError( "Input noise_pad must be a float/int, a CorrelatedNoise, Image, or filename "+ "containing an image to use to make a CorrelatedNoise!") pad_image.addNoise(cn) # Now we have to check: was the padding determined using pad_factor? Or by passing in an # image for padding? Treat these cases differently: # (1) If the former, then we can simply have the C++ handle the padding process. # (2) If the latter, then we have to do the padding ourselves, and pass the resulting image # to the C++ with pad_factor explicitly set to 1. if specify_size is False: # Make the SBInterpolatedImage out of the image. sbinterpolatedimage = galsim.SBInterpolatedImage( image, xInterp=self.x_interpolant, kInterp=self.k_interpolant, dx=dx, pad_factor=pad_factor, pad_image=pad_image, gsparams=gsparams) self.x_size = padded_size self.y_size = padded_size else: # Leave the original image as-is. Instead, we shift around the image to be used for # padding. Find out how much x and y margin there should be on lower end: x_marg = int(np.round(0.5*deltax)) y_marg = int(np.round(0.5*deltay)) # Now reset the pad_image to contain the original image in an even way pad_image = pad_image.view() pad_image.setScale(dx) pad_image.setOrigin(image.getXMin()-x_marg, image.getYMin()-y_marg) # Set the central values of pad_image to be equal to the input image pad_image[image.bounds] = image sbinterpolatedimage = galsim.SBInterpolatedImage( pad_image, xInterp=self.x_interpolant, kInterp=self.k_interpolant, dx=dx, pad_factor=1., gsparams=gsparams) self.x_size = 1+pad_image.getXMax()-pad_image.getXMin() self.y_size = 1+pad_image.getYMax()-pad_image.getYMin() # GalSim cannot automatically know what stepK and maxK are appropriate for the # input image. So it is usually worth it to do a manual calculation here. if calculate_stepk: sbinterpolatedimage.calculateStepK() if calculate_maxk: sbinterpolatedimage.calculateMaxK() # If the user specified a flux, then set to that flux value. if flux != None: if type(flux) != flux: flux = float(flux) sbinterpolatedimage.setFlux(flux) # If the user specified a flux normalization for the input Image, then since # SBInterpolatedImage works in terms of surface brightness, have to rescale the values to # get proper normalization. elif flux is None and normalization.lower() in ['flux','f'] and dx != 1.: sbinterpolatedimage.scaleFlux(1./(dx**2)) # If the input Image normalization is 'sb' then since that is the SBInterpolated default # assumption, no rescaling is needed. # Initialize the SBProfile GSObject.__init__(self, sbinterpolatedimage) # Fix the center to be in the right place. # Note the minus sign in front of image.scale, since we want to fix the center in the # opposite sense of what the draw function does. if use_true_center: prof = self._fix_center(image, -image.scale) GSObject.__init__(self, prof.SBProfile)
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)
def AddNoisePhot(im, weight_im, noise, base, rng, sky_level_pixel, logger=None): """ Add noise to an image according to the noise specifications in the noise dict appropriate for an image that has been drawn using the photon-shooting method. """ if not isinstance(noise, dict): raise AttributeError("image.noise is not a dict.") if 'type' not in noise: noise['type'] = 'Poisson' # Default is Poisson type = noise['type'] # First add the sky noise, if provided if sky_level_pixel: im += sky_level_pixel if type == 'Gaussian': single = [{'sigma': float, 'variance': float}] params = galsim.config.GetAllParams(noise, 'noise', base, single=single)[0] if 'sigma' in params: sigma = params['sigma'] else: import math sigma = math.sqrt(params['variance']) im.addNoise(galsim.GaussianNoise(rng, sigma=sigma)) if weight_im: weight_im += sigma * sigma if logger: logger.debug(' Added Gaussian noise with sigma = %f', sigma) elif type == 'Poisson': opt = {} if sky_level_pixel: opt['sky_level'] = float opt['sky_level_pixel'] = float else: single = [{'sky_level': float, 'sky_level_pixel': float}] sky_level_pixel = 0. # Switch from None to 0. params = galsim.config.GetAllParams(noise, 'noise', base, opt=opt, single=single)[0] if 'sky_level' in params and 'sky_level_pixel' in params: raise AttributeError( "Only one of sky_level and sky_level_pixel is allowed for " "noise.type = %s" % type) if 'sky_level' in params: pixel_scale = im.scale sky_level_pixel += params['sky_level'] * pixel_scale**2 if 'sky_level_pixel' in params: sky_level_pixel += params['sky_level_pixel'] # We don't have an exact value for the variance in each pixel, but the drawn image # before adding the Poisson noise is our best guess for the variance from the # object's flux, so just use that for starters. if weight_im and include_obj_var: weight_im.copyFrom(im) # For photon shooting, galaxy already has Poisson noise, so we want # to make sure not to add that again! if sky_level_pixel != 0.: im.addNoise( galsim.DeviateNoise( galsim.PoissonDeviate(rng, mean=sky_level_pixel))) im -= sky_level_pixel if weight_im: weight_im += sky_level_pixel if logger: logger.debug(' Added Poisson noise with sky_level_pixel = %f', sky_level_pixel) elif type == 'CCD': opt = {'gain': float, 'read_noise': float} if sky_level_pixel: opt['sky_level'] = float opt['sky_level_pixel'] = float else: single = [{'sky_level': float, 'sky_level_pixel': float}] sky_level_pixel = 0. # Switch from None to 0. params = galsim.config.GetAllParams(noise, 'noise', base, opt=opt, single=single)[0] if 'sky_level' in params and 'sky_level_pixel' in params: raise AttributeError( "Only one of sky_level and sky_level_pixel is allowed for " "noise.type = %s" % type) if 'sky_level' in params: pixel_scale = im.scale sky_level_pixel += params['sky_level'] * pixel_scale**2 if 'sky_level_pixel' in params: sky_level_pixel += params['sky_level_pixel'] gain = params.get('gain', 1.0) read_noise = params.get('read_noise', 0.0) # We don't have an exact value for the variance in each pixel, but the drawn image # before adding the Poisson noise is our best guess for the variance from the # object's flux, so just use that for starters. if weight_im and include_obj_var: weight_im.copyFrom(im) # For photon shooting, galaxy already has Poisson noise, so we want # to make sure not to add that again! if sky_level_pixel != 0.: if gain != 1.0: im *= gain im.addNoise( galsim.DeviateNoise( galsim.PoissonDeviate(rng, mean=sky_level_pixel * gain))) if gain != 1.0: im /= gain im -= sky_level_pixel * gain if read_noise != 0.: im.addNoise(galsim.GaussianNoise(rng, sigma=read_noise)) # Add in these effects to the weight image: if weight_im: import math if sky_level_pixel != 0.0 or read_noise != 0.0: weight_im += sky_level_pixel / gain + read_noise * read_noise if logger: logger.debug( ' Added CCD noise with sky_level_pixel = %f, ' + 'gain = %f, read_noise = %f', sky_level_pixel, gain, read_noise) elif type == 'COSMOS': req = {'file_name': str} opt = {'dx_cosmos': float, 'variance': float} kwargs = galsim.config.GetAllParams(noise, 'noise', base, req=req, opt=opt)[0] # Build and add the correlated noise cn = galsim.correlatednoise.getCOSMOSNoise(rng, **kwargs) im.addNoise(cn) # Then add the variance to the weight image, using the zero-lag correlation function value if weight_im: weight_im += cn.getVariance() if logger: logger.debug(' Added COSMOS correlated noise with variance = %f', cn.getVariance()) else: raise AttributeError("Invalid type %s for noise", type)
def allDetectorEffects(img, prev_exposures=(), rng=None, exptime=default_exptime): """ This utility applies all sources of noise and detector effects for WFIRST that are implemented in GalSim. In terms of noise, this includes the Poisson noise due to the signal (sky + background), dark current, and read noise. The detector effects that are included are reciprocity failure, quantization, persistence, nonlinearity, and interpixel capacitance. It also includes the necessary factors of gain. In short, the user should be able to pass in an Image with all sources of signal (background plus astronomical objects), and the Image will be modified to include all subsequent steps in the image generation process for WFIRST that are implemented in GalSim. However, to include the effect of persistence, the user needs to provide a list of up to {ncoeff} recent exposures (without the readout effects such nonlinearity and interpixel capacitance included) and the routine returns an updated list of up to {ncoeff} recent exposures. @param img The Image to be modified. @param prev_exposures List of up to {ncoeff} Image instances in the order of exposures, with the recent exposure being the first element. [default: []] @param rng An optional galsim.BaseDeviate to use for the addition of noise. If None, a new one will be initialized. [default: None] @param exptime The exposure time, in seconds. If None, then the WFIRST default exposure time will be used. [default: {exptime}] @returns prev_exposures Updated list of previous exposures containing up to {ncoeff} Image instances. """.format(ncoeff=len(galsim.wfirst.persistence_coefficients), exptime=default_exptime) # Make sure we don't have any negative values. img.replaceNegative(0.) # Add Poisson noise. rng = galsim.BaseDeviate(rng) poisson_noise = galsim.PoissonNoise(rng) img.addNoise(poisson_noise) # Quantize: have an integer number of photons in every pixel after inclusion of sky noise. img.quantize() # Reciprocity failure (use WFIRST routine, with the supplied exposure time). addReciprocityFailure(img, exptime=exptime) # Dark current (use exposure time). dark_current = galsim.wfirst.dark_current * exptime dark_noise = galsim.DeviateNoise(galsim.PoissonDeviate(rng, dark_current)) img.addNoise(dark_noise) # Persistence (use WFIRST coefficients) prev_exposures = list(prev_exposures) applyPersistence(img, prev_exposures) # Update the 'prev_exposures' queue ncoeff = len(galsim.wfirst.persistence_coefficients) prev_exposures = [img.copy()] + prev_exposures[:ncoeff - 1] # Nonlinearity (use WFIRST routine). applyNonlinearity(img) # IPC (use WFIRST routine). applyIPC(img) # Read noise. read_noise = galsim.GaussianNoise(rng, sigma=galsim.wfirst.read_noise) img.addNoise(read_noise) # Gain. img /= galsim.wfirst.gain # Quantize. img.quantize() return prev_exposures
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_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_uncorr_padding(): """Test for uncorrelated noise padding of InterpolatedImage.""" import time t1 = time.time() # Set up some defaults: use weird image sizes / shapes and noise variances. decimal_precise=5 decimal_coarse=2 orig_nx = 147 orig_ny = 174 noise_var = 1.73 big_nx = 519 big_ny = 482 orig_seed = 151241 # first, make a noise image orig_img = galsim.ImageF(orig_nx, orig_ny, scale=1.) gd = galsim.GaussianDeviate(orig_seed, mean=0., sigma=np.sqrt(noise_var)) orig_img.addNoise(galsim.DeviateNoise(gd)) # make it into an InterpolatedImage with some zero-padding # (note that default is zero-padding, by factors of several) int_im = galsim.InterpolatedImage(orig_img) # draw into a larger image big_img = galsim.ImageF(big_nx, big_ny) int_im.draw(big_img, scale=1.) # check that variance is diluted by expected amount - should be exact, so check precisely! # Note that this only works if the big image has the same even/odd-ness in the two sizes. # Otherwise the center of the original image will fall between pixels in the big image. # Then the variance will be smoothed somewhat by the interpolant. big_var_expected = np.var(orig_img.array)*float(orig_nx*orig_ny)/(big_nx*big_ny) np.testing.assert_almost_equal( np.var(big_img.array), big_var_expected, decimal=decimal_precise, err_msg='Variance not diluted by expected amount when zero-padding') # make it into an InterpolatedImage with noise-padding int_im = galsim.InterpolatedImage(orig_img, noise_pad=noise_var, noise_pad_size=max(big_nx,big_ny), rng = galsim.GaussianDeviate(orig_seed)) # draw into a larger image big_img = galsim.ImageF(big_nx, big_ny) int_im.draw(big_img, scale=1.) # check that variance is same as original - here, we cannot be too precise because the padded # region is not huge and the comparison will be, well, noisy. np.testing.assert_almost_equal( np.var(big_img.array), noise_var, decimal=decimal_coarse, err_msg='Variance not correct after padding image with noise') # check that if we pass in a RNG, it is actually used to pad with the same noise field # basically, redo all of the above steps and draw into a new image, make sure it's the same as # previous. int_im = galsim.InterpolatedImage(orig_img, noise_pad=noise_var, noise_pad_size=max(big_nx,big_ny), rng = galsim.GaussianDeviate(orig_seed)) big_img_2 = galsim.ImageF(big_nx, big_ny) int_im.draw(big_img_2, scale=1.) np.testing.assert_array_almost_equal( big_img_2.array, big_img.array, decimal=decimal_precise, err_msg='Cannot reproduce noise-padded image with same choice of seed') # Finally check inputs: what if we give it an input variance that is neg? A list? try: np.testing.assert_raises(ValueError,galsim.InterpolatedImage,orig_img,noise_pad=-1.) except ImportError: print 'The assert_raises tests require nose' t2 = time.time() print 'time for %s = %.2f'%(funcname(),t2-t1)
def main(argv): """ Make a fits image cube where each frame has two images of the same galaxy drawn with regular FFT convolution and with photon shooting. We do this for 5 different PSFs and 5 different galaxies, each with 4 different (random) fluxes, sizes, and shapes. """ logging.basicConfig(format="%(message)s", level=logging.INFO, stream=sys.stdout) logger = logging.getLogger("demo7") # To turn off logging: #logger.propagate = False # To turn on the debugging messages: #logger.setLevel(logging.DEBUG) # Define some parameters we'll use below. # Make output directory if not already present. if not os.path.isdir('output'): os.mkdir('output') file_name = os.path.join('output', 'cube_phot.fits.gz') random_seed = 553728 sky_level = 1.e4 # ADU / arcsec^2 pixel_scale = 0.28 # arcsec nx = 64 ny = 64 gal_flux_min = 1.e4 # Range for galaxy flux gal_flux_max = 1.e5 gal_hlr_min = 0.3 # arcsec gal_hlr_max = 1.3 # arcsec gal_e_min = 0. # Range for ellipticity gal_e_max = 0.8 psf_fwhm = 0.65 # arcsec # This script is set up as a comparison between using FFTs for doing the convolutions and # shooting photons. The two methods have trade-offs in speed and accuracy which vary # with the kind of profile being drawn and the S/N of the object, among other factors. # In addition, for each method, there are a number of parameters GalSim uses that control # aspects of the calculation that further affect the speed and accuracy. # # We encapsulate these parameters with an object called GSParams. The default values # are intended to be accurate enough for normal precision shear tests, without sacrificing # too much speed. # # Any PSF or galaxy object can be given a gsparams argument on construction that can # have different values to make the calculation more or less accurate (typically trading # off for speed or memory). # # In this script, we adjust some of the values slightly, just to show you how it works. # You could play around with these values and see what effect they have on the drawn images. # Usually, it requires a pretty drastic change in these parameters for you to be able to # notice the difference by eye. But subtle effects that may impact the shapes of galaxies # can happen well before then. # Type help(galsim.GSParams) for the complete list of parameters and more detailed # documentation, including the default values for each parameter. gsparams = galsim.GSParams( folding_threshold= 1.e-2, # maximum fractional flux that may be folded around edge of FFT maxk_threshold= 2.e-3, # k-values less than this may be excluded off edge of FFT xvalue_accuracy= 1.e-4, # approximations in real space aim to be this accurate kvalue_accuracy= 1.e-4, # approximations in fourier space aim to be this accurate shoot_accuracy= 1.e-4, # approximations in photon shooting aim to be this accurate minimum_fft_size=64) # minimum size of ffts logger.info('Starting demo script 7') # Make the PSF profiles: psf1 = galsim.Gaussian(fwhm=psf_fwhm, gsparams=gsparams) psf2 = galsim.Moffat(fwhm=psf_fwhm, beta=2.4, gsparams=gsparams) psf3_inner = galsim.Gaussian(fwhm=psf_fwhm, flux=0.8, gsparams=gsparams) psf3_outer = galsim.Gaussian(fwhm=2 * psf_fwhm, flux=0.2, gsparams=gsparams) psf3 = psf3_inner + psf3_outer atmos = galsim.Gaussian(fwhm=psf_fwhm, gsparams=gsparams) # The OpticalPSF and set of Zernike values chosen below correspond to a reasonably well aligned, # smallish ~0.3m / 12 inch diameter telescope with a central obscuration of ~0.12m or 5 inches # diameter, being used in optical wavebands. # In the Noll convention, the value of the Zernike coefficient also gives the RMS optical path # difference across a circular pupil. An RMS difference of ~0.5 or larger indicates that parts # of the wavefront are in fully destructive interference, and so we might expect aberrations to # become strong when Zernike aberrations summed in quadrature approach 0.5 wave. # The aberrations chosen in this case correspond to operating close to a 0.25 wave RMS optical # path difference. Unlike in demo3, we specify the aberrations by making a list that we pass # in using the 'aberrations' kwarg. The order of aberrations starting from index 4 is defocus, # astig1, astig2, coma1, coma2, trefoil1, trefoil2, spher as in the Noll convention. # We ignore the first 4 values so that the index number corresponds to the Zernike index # in the Noll convention. This will be particularly convenient once we start allowing # coefficients beyond spherical (index 11). c.f. The Wikipedia page about the Noll indices: # # http://en.wikipedia.org/wiki/Zernike_polynomials#Zernike_polynomials aberrations = [0.0] * 12 # Set the initial size. aberrations[4] = 0.06 # Noll index 4 = Defocus aberrations[5:7] = [0.12, -0.08] # Noll index 5,6 = Astigmatism aberrations[7:9] = [0.07, 0.04] # Noll index 7,8 = Coma aberrations[11] = -0.13 # Noll index 11 = Spherical # You could also define these all at once if that is more convenient: #aberrations = [0.0, 0.0, 0.0, 0.0, 0.06, 0.12, -0.08, 0.07, 0.04, 0.0, 0.0, -0.13] optics = galsim.OpticalPSF(lam_over_diam=0.6 * psf_fwhm, obscuration=0.4, aberrations=aberrations, gsparams=gsparams) psf4 = galsim.Convolve([atmos, optics ]) # Convolve inherits the gsparams from the first # item in the list. (Or you can supply a gsparams # argument explicitly if you want to override this.) atmos = galsim.Kolmogorov(fwhm=psf_fwhm, gsparams=gsparams) optics = galsim.Airy(lam_over_diam=0.3 * psf_fwhm, gsparams=gsparams) psf5 = galsim.Convolve([atmos, optics]) psfs = [psf1, psf2, psf3, psf4, psf5] psf_names = [ "Gaussian", "Moffat", "Double Gaussian", "OpticalPSF", "Kolmogorov * Airy" ] psf_times = [0, 0, 0, 0, 0] psf_fft_times = [0, 0, 0, 0, 0] psf_phot_times = [0, 0, 0, 0, 0] # Make the galaxy profiles: gal1 = galsim.Gaussian(half_light_radius=1, gsparams=gsparams) gal2 = galsim.Exponential(half_light_radius=1, gsparams=gsparams) gal3 = galsim.DeVaucouleurs(half_light_radius=1, gsparams=gsparams) gal4 = galsim.Sersic(half_light_radius=1, n=2.5, gsparams=gsparams) # A Sersic profile may be truncated if desired. # The units for this are expected to be arcsec (or specifically -- whatever units # you are using for all the size values as defined by the pixel_scale). bulge = galsim.Sersic(half_light_radius=0.7, n=3.2, trunc=8.5, gsparams=gsparams) disk = galsim.Sersic(half_light_radius=1.2, n=1.5, gsparams=gsparams) gal5 = 0.4 * bulge + 0.6 * disk # Net half-light radius is only approximate for this one. gals = [gal1, gal2, gal3, gal4, gal5] gal_names = [ "Gaussian", "Exponential", "Devaucouleurs", "n=2.5 Sersic", "Bulge + Disk" ] gal_times = [0, 0, 0, 0, 0] gal_fft_times = [0, 0, 0, 0, 0] gal_phot_times = [0, 0, 0, 0, 0] # Other times to keep track of: setup_times = 0 fft_times = 0 phot_times = 0 noise_times = 0 # Loop over combinations of psf, gal, and make 4 random choices for flux, size, shape. all_images = [] k = 0 for ipsf in range(len(psfs)): psf = psfs[ipsf] psf_name = psf_names[ipsf] logger.info('psf %d: %s', ipsf + 1, psf) # Note that this implicitly calls str(psf). We've made an effort to give all GalSim # objects an informative but relatively succinct str representation. Some details may # be missing, but it should look essentially like how you would create the object. logger.debug('repr = %r', psf) # The repr() version are a bit more pedantic in form and should be completely informative, # to the point where two objects that are not identical should never have equal repr # strings. As such the repr strings may in some cases be somewhat unwieldy. For instance, # since we set non-default gsparams in these, the repr includes that information, but # it is omitted from the str for brevity. for igal in range(len(gals)): gal = gals[igal] gal_name = gal_names[igal] logger.info(' galaxy %d: %s', igal + 1, gal) logger.debug(' repr = %r', gal) for i in range(4): logger.debug(' Start work on image %d', i) t1 = time.time() # Initialize the random number generator we will be using. rng = galsim.UniformDeviate(random_seed + k) # Generate random variates: flux = rng() * (gal_flux_max - gal_flux_min) + gal_flux_min # Use a new variable name, since we'll want to keep the original unmodified. this_gal = gal.withFlux(flux) hlr = rng() * (gal_hlr_max - gal_hlr_min) + gal_hlr_min this_gal = this_gal.dilate(hlr) beta_ellip = rng() * 2 * math.pi * galsim.radians ellip = rng() * (gal_e_max - gal_e_min) + gal_e_min gal_shape = galsim.Shear(e=ellip, beta=beta_ellip) this_gal = this_gal.shear(gal_shape) # Build the final object by convolving the galaxy and PSF. final = galsim.Convolve([this_gal, psf]) # Create the large, double width output image # Rather than provide a scale= argument to the drawImage commands, we can also # set the pixel scale in the image constructor. # Note: You can also change it after the construction with im.scale=pixel_scale image = galsim.ImageF(2 * nx + 2, ny, scale=pixel_scale) # Assign the following two Image "views", fft_image and phot_image. # Using the syntax below, these are views into the larger image. # Changes/additions to the sub-images referenced by the views are automatically # reflected in the original image. fft_image = image[galsim.BoundsI(1, nx, 1, ny)] phot_image = image[galsim.BoundsI(nx + 3, 2 * nx + 2, 1, ny)] logger.debug( ' Read in training sample galaxy and PSF from file') t2 = time.time() # Draw the profile # This default rendering method (method='auto') usually defaults to FFT, since # that is normally the most efficient method. However, we can also set method # to 'fft' explicitly to force it to always use FFTs for the convolution # by the pixel response. (In this case, it doesn't have any effect, since # the 'auto' method would have always chosen 'fft' anyway, so this is just # for illustrative purposes.) final.drawImage(fft_image, method='fft') logger.debug( ' Drew fft image. Total drawn flux = %f. .flux = %f', fft_image.array.sum(), final.getFlux()) t3 = time.time() # Add Poisson noise sky_level_pixel = sky_level * pixel_scale**2 fft_image.addNoise( galsim.PoissonNoise(rng, sky_level=sky_level_pixel)) t4 = time.time() # The next two lines are just to get the output from this demo script # to match the output from the parsing of demo7.yaml. rng = galsim.UniformDeviate(random_seed + k) rng() rng() rng() rng() # Repeat for photon shooting image. # The max_extra_noise parameter indicates how much extra noise per pixel we are # willing to tolerate. The sky noise will be adding a variance of sky_level_pixel, # so we allow up to 1% of that extra. final.drawImage(phot_image, method='phot', max_extra_noise=sky_level_pixel / 100, rng=rng) t5 = time.time() # For photon shooting, galaxy already has Poisson noise, so we want to make # sure not to add that noise again! Thus, we just add sky noise, which # is Poisson with the mean = sky_level_pixel pd = galsim.PoissonDeviate(rng, mean=sky_level_pixel) # DeviateNoise just adds the action of the given deviate to every pixel. phot_image.addNoise(galsim.DeviateNoise(pd)) # For PoissonDeviate, the mean is not zero, so for a background-subtracted # image, we need to subtract the mean back off when we are done. phot_image -= sky_level_pixel logger.debug( ' Added Poisson noise. Image fluxes are now %f and %f', fft_image.array.sum(), phot_image.array.sum()) t6 = time.time() # Store that into the list of all images all_images += [image] k = k + 1 logger.info( ' %d: flux = %.2e, hlr = %.2f, ellip = (%.2f,%.2f)', k, flux, hlr, gal_shape.getE1(), gal_shape.getE2()) logger.debug(' Times: %f, %f, %f, %f, %f', t2 - t1, t3 - t2, t4 - t3, t5 - t4, t6 - t5) psf_times[ipsf] += t6 - t1 psf_fft_times[ipsf] += t3 - t2 psf_phot_times[ipsf] += t5 - t4 gal_times[igal] += t6 - t1 gal_fft_times[igal] += t3 - t2 gal_phot_times[igal] += t5 - t4 setup_times += t2 - t1 fft_times += t3 - t2 phot_times += t5 - t4 noise_times += t4 - t3 + t6 - t5 logger.info('Done making images of galaxies') logger.info('') logger.info('Some timing statistics:') logger.info(' Total time for setup steps = %f', setup_times) logger.info(' Total time for regular fft drawing = %f', fft_times) logger.info(' Total time for photon shooting = %f', phot_times) logger.info(' Total time for adding noise = %f', noise_times) logger.info('') logger.info('Breakdown by PSF type:') for ipsf in range(len(psfs)): logger.info(' %s: Total time = %f (fft: %f, phot: %f)', psf_names[ipsf], psf_times[ipsf], psf_fft_times[ipsf], psf_phot_times[ipsf]) logger.info('') logger.info('Breakdown by Galaxy type:') for igal in range(len(gals)): logger.info(' %s: Total time = %f (fft: %f, phot: %f)', gal_names[igal], gal_times[igal], gal_fft_times[igal], gal_phot_times[igal]) logger.info('') # Now write the image to disk. # With any write command, you can optionally compress the file using several compression # schemes: # 'gzip' uses gzip on the full output file. # 'bzip2' uses bzip2 on the full output file. # 'rice' uses rice compression on the image, leaving the fits headers readable. # 'gzip_tile' uses gzip in tiles on the output image, leaving the fits headers readable. # 'hcompress' uses hcompress on the image, but it is only valid for 2-d data, so it # doesn't work for writeCube. # 'plio' uses plio on the image, but it is only valid for positive integer data. # Furthermore, the first three have standard filename extensions associated with them, # so if you don't specify a compression, but the filename ends with '.gz', '.bz2' or '.fz', # the corresponding compression will be selected automatically. # In other words, the `compression='gzip'` specification is actually optional here: galsim.fits.writeCube(all_images, file_name, compression='gzip') logger.info('Wrote fft image to fits data cube %r', file_name)
def func(file_name, random_seed, pixel_scale, nx, ny, sky_level, gal_flux_min, gal_flux_max, gal_hlr_min, gal_hlr_max, gal_e_min, gal_e_max, psf_fwhm, gsparams, psf1, psf2, psf3_inner, psf3_outer, psf3, atmos, aberrations, psf4, optics, psf5, psfs, psf_names, psf_times, psf_fft_times, psf_phot_times, gal1, gal2, gal3, gal4, bulge, disk, gal5, gals, gal_names, gal_times, gal_fft_times, gal_phot_times, setup_times, fft_times, phot_times, noise_times, k, all_fluxes_i, psf, psf_name, gal, gal_name): #Functioning #---> start here logger = logging.getLogger("demo7") ### this willl return the time in seconds t1 = time.time() ### generate randomly a number # Initialize the random number generator we will be using. rng = galsim.UniformDeviate(random_seed+k+1) ### use the random numbers for the flux # Generate random variates: flux = all_fluxes_i y_i = flux # Use a new variable name, since we'll want to keep the original unmodified. this_gal = gal.withFlux(flux) ### use the random numbers for the hlr hlr = rng() * (gal_hlr_max-gal_hlr_min) + gal_hlr_min # Use a new variable name, since we'll want to keep the original unmodified. this_gal = this_gal.dilate(hlr) ### use the random numbers for the ellipticity 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) # Use a new variable name, since we'll want to keep the original unmodified. this_gal = this_gal.shear(gal_shape) ### build the final object by combinging the galaxy and psf final = galsim.Convolve([this_gal, psf]) ### create the images and save them in an empty array image = galsim.ImageF(2*nx+2, ny, scale=pixel_scale) ### make a views subset of the larger 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') ### record time t2 = time.time() ### Draw the profile explicity through fft for the convolution final.drawImage(fft_image, method='fft') logger.debug(' Drew fft image. Total drawn flux = %f. .flux = %f', fft_image.array.sum(),final.getFlux()) ### record time t3 = time.time() ### Add Poisson noise to the fft image convolution sky_level_pixel = sky_level * pixel_scale**2 fft_image.addNoise(galsim.PoissonNoise(rng, sky_level=sky_level_pixel)) ### record time 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 final.drawImage(phot_image, method='phot', max_extra_noise=sky_level_pixel/100, rng=rng) ### record time 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()) ### record time t6 = time.time() #<----end here (return stuff) return image, t1, t2, t3, t4, t5, t6, k, flux, hlr, gal_shape, y_i, psfs, gals, file_name
phot_image = image[galsim.BoundsI(nx + 3, 2 * nx + 2, 1, ny)] final.drawImage(fft_image, method='fft') sky_level_pixel = sky_level * pixel_scale**2 fft_image.addNoise(galsim.PoissonNoise( rng, sky_level=sky_level_pixel)) #add noise rng = galsim.UniformDeviate(random_seed + k + 1) rng() rng() rng() rng() final.drawImage(phot_image,method='phot',max_extra_noise=sky_level_pixel/100,\ rng=rng) pd = galsim.PoissonDeviate(rng, mean=sky_level_pixel) phot_image.addNoise(galsim.DeviateNoise(pd)) phot_image -= sky_level_pixel imagea = image.array galarray1.append(imagea[0:64, 0:64]) for i in range(n): k += 1 rng = galsim.UniformDeviate(random_seed + k + 1) flux = rng() * (gal_flux_max - gal_flux_min) + gal_flux_min this_gal = gal2.withFlux(flux) hlr = rng() * (gal_hlr_max - gal_hlr_min) + gal_hlr_min this_gal = this_gal.dilate(hlr) this_gal = this_gal.shear(e1=e10[i], e2=e20[i]) #.shear(g1=gm1,g2=gm2) final = galsim.Convolve([this_gal, psf]) print(rng())
def test_gaussian_noise(): """Test Gaussian random number generator """ gSigma = 17.23 g = galsim.GaussianDeviate(testseed, sigma=gSigma) gResult = np.empty((10, 10)) g.generate(gResult) noise = galsim.DeviateNoise(g) # Test filling an image testimage = galsim.ImageD(10, 10) noise.rng.seed(testseed) testimage.addNoise(noise) np.testing.assert_array_almost_equal( testimage.array, gResult, precision, err_msg= 'Wrong Gaussian random number sequence generated when applied to image.' ) # Test filling a single-precision image noise.rng.seed(testseed) testimage = galsim.ImageF(10, 10) testimage.addNoise(noise) np.testing.assert_array_almost_equal( testimage.array, gResult, precisionF, err_msg= 'Wrong Gaussian random number sequence generated when applied to ImageF.' ) # GaussianNoise is equivalent, but no mean allowed. gn = galsim.GaussianNoise(galsim.BaseDeviate(testseed), sigma=gSigma) testimage = galsim.ImageD(10, 10) testimage.addNoise(gn) np.testing.assert_array_almost_equal( testimage.array, gResult, precision, err_msg= "GaussianNoise applied to Images does not reproduce expected sequence") # Test filling an image with Fortran ordering gn.rng.seed(testseed) testimage = galsim.ImageD(np.zeros((10, 10)).T) testimage.addNoise(gn) np.testing.assert_array_almost_equal( testimage.array, gResult, precision, err_msg="Wrong Gaussian noise generated for Fortran-ordered Image") # Check GaussianNoise variance: np.testing.assert_almost_equal( gn.getVariance(), gSigma**2, precision, err_msg="GaussianNoise getVariance returns wrong variance") np.testing.assert_almost_equal( gn.sigma, gSigma, precision, err_msg="GaussianNoise sigma returns wrong value") # Check that the noise model really does produce this variance. big_im = galsim.Image(2048, 2048, dtype=float) gn.rng.seed(testseed) big_im.addNoise(gn) var = np.var(big_im.array) print('variance = ', var) print('getVar = ', gn.getVariance()) np.testing.assert_almost_equal( var, gn.getVariance(), 1, err_msg= 'Realized variance for GaussianNoise did not match getVariance()') # Check that GaussianNoise adds to the image, not overwrites the image. gal = galsim.Exponential(half_light_radius=2.3, flux=1.e4) gal.drawImage(image=big_im) gn.rng.seed(testseed) big_im.addNoise(gn) gal.withFlux(-1.e4).drawImage(image=big_im, add_to_image=True) var = np.var(big_im.array) np.testing.assert_almost_equal( var, gn.getVariance(), 1, err_msg='GaussianNoise wrong when already an object drawn on the image' ) # Check that DeviateNoise adds to the image, not overwrites the image. gal.drawImage(image=big_im) gn.rng.seed(testseed) big_im.addNoise(gn) gal.withFlux(-1.e4).drawImage(image=big_im, add_to_image=True) var = np.var(big_im.array) np.testing.assert_almost_equal( var, gn.getVariance(), 1, err_msg='DeviateNoise wrong when already an object drawn on the image') # Check withVariance gn = gn.withVariance(9.) np.testing.assert_almost_equal( gn.getVariance(), 9, precision, err_msg="GaussianNoise withVariance results in wrong variance") np.testing.assert_almost_equal( gn.sigma, 3., precision, err_msg="GaussianNoise withVariance results in wrong sigma") # Check withScaledVariance gn = gn.withScaledVariance(4.) np.testing.assert_almost_equal( gn.getVariance(), 36., precision, err_msg="GaussianNoise withScaledVariance results in wrong variance") np.testing.assert_almost_equal( gn.sigma, 6., precision, err_msg="GaussianNoise withScaledVariance results in wrong sigma") # Check arithmetic gn = gn.withVariance(0.5) gn2 = gn * 3 np.testing.assert_almost_equal( gn2.getVariance(), 1.5, precision, err_msg="GaussianNoise gn*3 results in wrong variance") np.testing.assert_almost_equal( gn.getVariance(), 0.5, precision, err_msg="GaussianNoise gn*3 results in wrong variance for original gn") gn2 = 5 * gn np.testing.assert_almost_equal( gn2.getVariance(), 2.5, precision, err_msg="GaussianNoise 5*gn results in wrong variance") np.testing.assert_almost_equal( gn.getVariance(), 0.5, precision, err_msg="GaussianNoise 5*gn results in wrong variance for original gn") gn2 = gn / 2 np.testing.assert_almost_equal( gn2.getVariance(), 0.25, precision, err_msg="GaussianNoise gn/2 results in wrong variance") np.testing.assert_almost_equal( gn.getVariance(), 0.5, precision, err_msg="GaussianNoise 5*gn results in wrong variance for original gn") gn *= 3 np.testing.assert_almost_equal( gn.getVariance(), 1.5, precision, err_msg="GaussianNoise gn*=3 results in wrong variance") gn /= 2 np.testing.assert_almost_equal( gn.getVariance(), 0.75, precision, err_msg="GaussianNoise gn/=2 results in wrong variance") # Check starting with GaussianNoise() gn2 = galsim.GaussianNoise() gn2 = gn2.withVariance(9.) np.testing.assert_almost_equal( gn2.getVariance(), 9, precision, err_msg="GaussianNoise().withVariance results in wrong variance") np.testing.assert_almost_equal( gn2.sigma, 3., precision, err_msg="GaussianNoise().withVariance results in wrong sigma") gn2 = galsim.GaussianNoise() gn2 = gn2.withScaledVariance(4.) np.testing.assert_almost_equal( gn2.getVariance(), 4., precision, err_msg="GaussianNoise().withScaledVariance results in wrong variance") np.testing.assert_almost_equal( gn2.sigma, 2., precision, err_msg="GaussianNoise().withScaledVariance results in wrong sigma") # Check picklability do_pickle(gn, lambda x: (x.rng.serialize(), x.sigma)) do_pickle(gn, drawNoise) do_pickle(gn) # Check copy, eq and ne gn = gn.withVariance(gSigma**2) gn2 = galsim.GaussianNoise(gn.rng.duplicate(), gSigma) gn3 = gn.copy() gn4 = gn.copy(rng=galsim.BaseDeviate(11)) gn5 = galsim.GaussianNoise(gn.rng, 2. * gSigma) assert gn == gn2 assert gn == gn3 assert gn != gn4 assert gn != gn5 assert gn.rng.raw() == gn2.rng.raw() assert gn == gn2 assert gn == gn3 gn.rng.raw() assert gn != gn2 assert gn == gn3
def allDetectorEffects(img, rng=None, exptime=None, prev_exposures=[]): """ This utility applies all sources of noise and detector effects for WFIRST that are implemented in GalSim. In terms of noise, this includes the Poisson noise due to the signal (sky + background), dark current, and read noise. The detector effects that are included are reciprocity failure, quantization, persistence, nonlinearity, and interpixel capacitance. It also includes the necessary factors of gain. In short, the user should be able to pass in an Image with all sources of signal (background plus astronomical objects), and the Image will be modified to include all subsequent steps in the image generation process for WFIRST that are implemented in GalSim. However, to include the effect of persistence, the user needs to provide a list of up to {ncoeff} recent exposures (without the readout effects such nonlinearity and interpixel capacitance included) and the routine returns an updated list of up to {ncoeff} recent exposures. @param img The Image to be modified. @param rng An optional galsim.BaseDeviate to use for the addition of noise. If None, a new one will be initialized. [default: None] @param exptime The exposure time, in seconds. If None, then the WFIRST default exposure time will be used. [default: None] @param prev_exposures List of up to {ncoeff} Image instances in the order of exposures, with the recent exposure being the first element. [default: []] @returns prev_exposures Updated list of previous exposures containing up to {ncoeff} Image instances. """.format(ncoeff=galsim.wfirst.persistence_coefficients) # Deal appropriately with passed-in RNG, exposure time. if rng is None: rng = galsim.BaseDeviate() elif not isinstance(rng, galsim.BaseDeviate): raise TypeError( "The rng provided to RealGalaxy constructor is not a BaseDeviate") if exptime is None: exptime = galsim.wfirst.exptime # Add Poisson noise. poisson_noise = galsim.PoissonNoise(rng) img.addNoise(poisson_noise) # Reciprocity failure (use WFIRST routine, with the supplied exposure time). addReciprocityFailure(img, exptime=exptime) # Quantize. img.quantize() # Dark current (use exposure time). dark_current = galsim.wfirst.dark_current * exptime dark_noise = galsim.DeviateNoise(galsim.PoissonDeviate(rng, dark_current)) img.addNoise(dark_noise) # Persistence (use WFIRST coefficients) applyPersistence(img, prev_exposures) # Update the 'prev_exposures' queue if len(prev_exposures) >= len(galsim.wfirst.persistence_coefficients): prev_exposures.pop() prev_exposures.insert(0, img.copy()) # Nonlinearity (use WFIRST routine). applyNonlinearity(img) # IPC (use WFIRST routine). applyIPC(img) # Read noise. read_noise = galsim.GaussianNoise(rng, sigma=galsim.wfirst.read_noise) img.addNoise(read_noise) # Gain. img /= galsim.wfirst.gain # Quantize. img.quantize() return prev_exposures
def test_deviate_noise(): """Test basic functionality of the DeviateNoise class """ u = galsim.UniformDeviate(testseed) uResult = np.empty((10, 10)) u.generate(uResult) noise = galsim.DeviateNoise(galsim.UniformDeviate(testseed)) # Test filling an image with random values testimage = galsim.ImageD(10, 10) testimage.addNoise(noise) np.testing.assert_array_almost_equal( testimage.array, uResult, precision, err_msg= 'Wrong uniform random number sequence generated when applied to image.' ) # Test filling a single-precision image noise.rng.seed(testseed) testimage = galsim.ImageF(10, 10) testimage.addNoise(noise) np.testing.assert_array_almost_equal( testimage.array, uResult, precisionF, err_msg= 'Wrong uniform random number sequence generated when applied to ImageF.' ) # Test filling an image with Fortran ordering noise.rng.seed(testseed) testimage = galsim.ImageD(np.zeros((10, 10)).T) testimage.addNoise(noise) np.testing.assert_array_almost_equal( testimage.array, uResult, precision, err_msg="Wrong uniform randoms generated for Fortran-ordered Image") # Check picklability do_pickle(noise, drawNoise) do_pickle(noise) # Check copy, eq and ne noise2 = galsim.DeviateNoise( noise.rng.duplicate()) # Separate but equivalent rng chain. noise3 = noise.copy() # Always has exactly the same rng as noise. noise4 = noise.copy( rng=galsim.BaseDeviate(11)) # Always has a different rng than noise assert noise == noise2 assert noise == noise3 assert noise != noise4 assert noise.rng() == noise2.rng() assert noise == noise2 # Still equal because both chains incremented one place. assert noise == noise3 # Still equal because noise 3's rng is always equal to noise's rng. noise.rng() assert noise2 != noise3 # This is no longer equal, since only noise.rng is incremented. assert noise == noise3 assert_raises(TypeError, galsim.DeviateNoise, 53) assert_raises(NotImplementedError, galsim.BaseNoise().getVariance) assert_raises(NotImplementedError, galsim.BaseNoise().withVariance, 23) assert_raises(NotImplementedError, galsim.BaseNoise().withScaledVariance, 23) assert_raises(TypeError, noise.applyTo, 23) assert_raises(NotImplementedError, galsim.BaseNoise().applyTo, testimage) assert_raises(galsim.GalSimError, noise.getVariance) assert_raises(galsim.GalSimError, noise.withVariance, 23) assert_raises(galsim.GalSimError, noise.withScaledVariance, 23)
def main(argv): # Where to find and output data. path, filename = os.path.split(__file__) outpath = os.path.abspath(os.path.join(path, "output/")) # Just use a few galaxies, to save time. Note that we are going to put 4000 galaxy images into # our big image, so if we have n_use=10, each galaxy will appear 400 times. Users who want a # more interesting image with greater variation in the galaxy population can change `n_use` to # something larger (but it should be <=100, the number of galaxies in this small example # catalog). With 4000 galaxies in a 4k x 4k image with the WFIRST pixel scale, the effective # galaxy number density is 74/arcmin^2. This is not the number density that is expected for a # sample that is so bright (I<23.5) but it makes the image more visually interesting. One could # think of it as what you'd get if you added up several images at once, making the images for a # sample that is much deeper have the same S/N as that for an I<23.5 sample in a single image. n_use = 10 n_tot = 4000 # Default is to use all filters. Specify e.g. 'YJH' to only do Y106, J129, and H158. use_filters = None # quick and dirty command line parsing. for var in argv: if var.startswith('data='): datapath = var[5:] if var.startswith('out='): outpath = var[4:] if var.startswith('nuse='): n_use = int(var[5:]) if var.startswith('ntot='): n_tot = int(var[5:]) if var.startswith('filters='): use_filters = var[8:].upper() # Make output directory if not already present. if not os.path.isdir(outpath): os.mkdir(outpath) # In non-script code, use getLogger(__name__) at module scope instead. logging.basicConfig(format="%(message)s", level=logging.INFO, stream=sys.stdout) logger = logging.getLogger("demo13") # Initialize (pseudo-)random number generator. random_seed = 123456 rng = galsim.BaseDeviate(random_seed) # Generate a Poisson noise model. poisson_noise = galsim.PoissonNoise(rng) logger.info('Poisson noise model created.') # Read in the WFIRST filters, setting an AB zeropoint appropriate for this telescope given its # diameter and (since we didn't use any keyword arguments to modify this) using the typical # exposure time for WFIRST images. By default, this routine truncates the parts of the # bandpasses that are near 0 at the edges, and thins them by the default amount. filters = wfirst.getBandpasses(AB_zeropoint=True) logger.debug('Read in WFIRST imaging filters.') logger.info('Reading from a parametric COSMOS catalog.') # Read in a galaxy catalog - just a random subsample of 100 galaxies for F814W<23.5 from COSMOS. cat_file_name = 'real_galaxy_catalog_23.5_example_fits.fits' dir = 'data' # Use the routine that can take COSMOS real or parametric galaxy information, and tell it we # want parametric galaxies that represent an I<23.5 sample. cat = galsim.COSMOSCatalog(cat_file_name, dir=dir, use_real=False) logger.info('Read in %d galaxies from catalog'%cat.nobjects) # Here we carry out the initial steps that are necessary to get a fully chromatic PSF. We use # the getPSF() routine in the WFIRST module, which knows all about the telescope parameters # (diameter, bandpasses, obscuration, etc.). Note that we arbitrarily choose a single SCA # (Sensor Chip Assembly) rather than all of them, for faster calculations, and use a simple # representation of the struts for faster calculations. To do a more exact calculation of the # chromaticity and pupil plane configuration, remove the `approximate_struts` and the `n_waves` # keyword from the call to getPSF(): use_SCA = 7 # This could be any number from 1...18 logger.info('Doing expensive pre-computation of PSF.') t1 = time.time() logger.setLevel(logging.DEBUG) # Need to make a separate PSF for each filter. We are, however, ignoring the # position-dependence of the PSF within each SCA, just using the PSF at the center of the SCA # (default kwargs). PSFs = {} for filter_name, filter_ in filters.items(): logger.info('PSF pre-computation for SCA %d, filter %s.'%(use_SCA, filter_name)) PSFs[filter_name] = wfirst.getPSF(use_SCA, filter_name, approximate_struts=True, n_waves=10, logger=logger) logger.setLevel(logging.INFO) t2 = time.time() logger.info('Done PSF precomputation in %.1f seconds!'%(t2-t1)) # Define the size of the postage stamp that we use for each individual galaxy within the larger # image, and for the PSF images. stamp_size = 256 # We choose a particular (RA, dec) location on the sky for our observation. ra_targ = 90.*galsim.degrees dec_targ = -10.*galsim.degrees targ_pos = galsim.CelestialCoord(ra=ra_targ, dec=dec_targ) # Get the WCS for an observation at this position. We are not supplying a date, so the routine # will assume it's the vernal equinox. We are also not supplying a position angle for the # observatory, which means that it will just find the optimal one (the one that has the solar # panels pointed most directly towards the Sun given this targ_pos and date). The output of # this routine is a dict of WCS objects, one for each SCA. We then take the WCS for the SCA # that we are using. wcs_list = wfirst.getWCS(world_pos=targ_pos, SCAs=use_SCA) wcs = wcs_list[use_SCA] # We need to find the center position for this SCA. We'll tell it to give us a CelestialCoord # corresponding to (X, Y) = (wfirst.n_pix/2, wfirst.n_pix/2). SCA_cent_pos = wcs.toWorld(galsim.PositionD(wfirst.n_pix/2, wfirst.n_pix/2)) # We randomly distribute points in (X, Y) on the CCD. # If we had a real galaxy catalog with positions in terms of RA, dec we could use wcs.toImage() # to find where those objects should be in terms of (X, Y). pos_rng = galsim.UniformDeviate(random_seed) # Make a list of (X, Y, F814W magnitude, n_rot, flip) values. # (X, Y) give the position of the galaxy centroid (or the center of the postage stamp into which # we draw the galaxy) in the big image. # F814W magnitudes are randomly drawn from the catalog, and are used to create a more realistic # flux distribution for the galaxies instead of just having the 10 flux values for the galaxies # we have chosen to draw. # n_rot says how many 90 degree rotations to include for a given realization of each galaxy, so # it doesn't appear completely identical each time we put it in the image. # flip is a random number that will determine whether we include an x-y flip for this appearance # of the galaxy or not. x_stamp = [] y_stamp = [] mag_stamp = [] n_rot_stamp = [] flip_stamp = [] for i_gal in range(n_tot): x_stamp.append(pos_rng()*wfirst.n_pix) y_stamp.append(pos_rng()*wfirst.n_pix) # Note that we could use wcs.toWorld() to get the (RA, dec) for these (x, y) positions. Or, # if we had started with (RA, dec) positions, we could have used wcs.toImage() to get the # CCD coordinates for those positions. mag_stamp.append(cat.param_cat['mag_auto'][int(pos_rng()*cat.nobjects)]) n_rot_stamp.append(int(4*pos_rng())) flip_stamp.append(pos_rng()) # Make the 2-component parametric GSObjects for each object, including chromaticity (roughly # appropriate SEDs per galaxy component, at the appropriate galaxy redshift). Note that since # the PSF is position-independent within the SCA, we can simply do the convolution with that PSF # now instead of using a different one for each position. We also have to include the correct # flux scaling: The catalog returns objects that would be observed by HST in 1 second # exposures. So for our telescope we scale up by the relative area and exposure time. Note that # what is important is the *effective* area after taking into account obscuration. logger.info('Processing the objects in the catalog to get GSObject representations') # Choose a random set of unique indices in the catalog (will be the same each time script is # run, due to use of the same random seed): rand_indices = [] while len(rand_indices)<n_use: tmp_ind = int(pos_rng()*cat.nobjects) if tmp_ind not in rand_indices: rand_indices.append(tmp_ind) obj_list = cat.makeGalaxy(rand_indices, chromatic=True, gal_type='parametric') hst_eff_area = 2.4**2 * (1.-0.33**2) wfirst_eff_area = galsim.wfirst.diameter**2 * (1.-galsim.wfirst.obscuration**2) flux_scaling = (wfirst_eff_area/hst_eff_area) * wfirst.exptime mag_list = [] for ind in range(len(obj_list)): # First, let's check what magnitude this object has in F814W. We want to do this because # (to inject some variety into our images) we are going to rescale the fluxes in all bands # for different instances of this galaxy in the final image in order to get a reasonable S/N # distribution. So we need to save the original magnitude in F814W, to compare with a # randomly drawn one from the catalog. This is not something that most users would need to # do. mag_list.append(cat.param_cat['mag_auto'][cat.orig_index[rand_indices[ind]]]) # Calculate the sky level for each filter, and draw the PSF and the galaxies through the # filters. for filter_name, filter_ in filters.items(): if use_filters is not None and filter_name[0] not in use_filters: logger.info('Skipping filter {0}.'.format(filter_name)) continue logger.info('Beginning work for {0}.'.format(filter_name)) # Drawing PSF. Note that the PSF object intrinsically has a flat SED, so if we convolve it # with a galaxy, it will properly take on the SED of the galaxy. For the sake of this demo, # we will simply convolve with a 'star' that has a flat SED and unit flux in this band, so # that the PSF image will be normalized to unit flux. This does mean that the PSF image # being drawn here is not quite the right PSF for the galaxy. Indeed, the PSF for the # galaxy effectively varies within it, since it differs for the bulge and the disk. To make # a real image, one would have to choose SEDs for stars and convolve with a star that has a # reasonable SED, but we just draw with a flat SED for this demo. out_filename = os.path.join(outpath, 'demo13_PSF_{0}.fits'.format(filter_name)) # Generate a point source. point = galsim.DeltaFunction(flux=1.) # Use a flat SED here, but could use something else. A stellar SED for instance. # Or a typical galaxy SED. Depending on your purpose for drawing the PSF. star_sed = galsim.SED(lambda x:1, 'nm', 'flambda').withFlux(1.,filter_) # Give it unit flux in this filter. star = galsim.Convolve(point*star_sed, PSFs[filter_name]) img_psf = galsim.ImageF(64,64) star.drawImage(bandpass=filter_, image=img_psf, scale=wfirst.pixel_scale) img_psf.write(out_filename) logger.debug('Created PSF with flat SED for {0}-band'.format(filter_name)) # Set up the full image that will contain all the individual galaxy images, with information # about WCS: final_image = galsim.ImageF(wfirst.n_pix,wfirst.n_pix, wcs=wcs) # Draw the galaxies into the image. for i_gal in range(n_use): logger.info('Drawing image for the object at row %d in the input catalog'%i_gal) # We want to only draw the galaxy once (for speed), not over and over with different # sub-pixel offsets. For this reason we ignore the sub-pixel offset entirely. Note # that we are setting the postage stamp to have the average WFIRST pixel scale. This is # simply an approximation for the purpose of speed; really, one should draw directly # into final_image, which has the appropriate WCS for WFIRST. In that case, the image # of the galaxy might look different in different parts of the detector due to the WCS # (including distortion), and we would have to re-draw each time. To keep the demo # relatively quick, we are just using the approximate average pixel scale and drawing # once. stamp = galsim.Image(stamp_size, stamp_size, scale=wfirst.pixel_scale) # Convolve the chromatic galaxy and the chromatic PSF for this bandpass, and rescale flux. final = galsim.Convolve(flux_scaling*obj_list[ind], PSFs[filter_name]) final.drawImage(filter_, image=stamp) # Have to find where to place it: for i_gal_use in range(i_gal*n_tot//n_use, (i_gal+1)*n_tot//n_use): # Account for the fractional part of the position: ix = int(math.floor(x_stamp[i_gal_use]+0.5)) iy = int(math.floor(y_stamp[i_gal_use]+0.5)) # We don't actually use this offset. offset = galsim.PositionD(x_stamp[i_gal]-ix, y_stamp[i_gal]-iy) # Create a nominal bound for the postage stamp given the integer part of the # position. stamp_bounds = galsim.BoundsI(ix-0.5*stamp_size, ix+0.5*stamp_size-1, iy-0.5*stamp_size, iy+0.5*stamp_size-1) # Find the overlapping bounds between the large image and the individual postage # stamp. bounds = stamp_bounds & final_image.bounds # Just to inject a bit of variety into the image, so it isn't *quite* as obvious # that we've repeated the same 10 objects over and over, we randomly rotate the # postage stamp by some factor of 90 degrees and possibly include a random flip. if flip_stamp[i_gal_use] > 0.5: new_arr = numpy.ascontiguousarray( numpy.rot90(stamp.array, n_rot_stamp[i_gal_use])) else: new_arr = numpy.ascontiguousarray( numpy.fliplr(numpy.rot90(stamp.array, n_rot_stamp[i_gal_use]))) stamp_rot = galsim.Image(new_arr, scale=stamp.scale) stamp_rot.setOrigin(galsim.PositionI(stamp_bounds.xmin, stamp_bounds.ymin)) # Rescale the flux to match that of a randomly chosen galaxy in the galaxy, but # keeping the same SED as for this particular galaxy. This gives a bit more # variety in the flux values and SNR of the galaxies in the image without having # to render images of many more objects. flux_scaling = 10**(-0.4*(mag_stamp[i_gal_use]-mag_list[i_gal])) # Copy the image into the right place in the big image. final_image[bounds] += flux_scaling * stamp_rot[bounds] # Now we're done with the per-galaxy drawing for this image. The rest will be done for the # entire image at once. logger.info('Postage stamps of all galaxies drawn on a single big image for this filter.') logger.info('Adding the sky level, noise and detector non-idealities.') # First we get the amount of zodaical light for a position corresponding to the center of # this SCA. The results are provided in units of e-/arcsec^2, using the default WFIRST # exposure time since we did not explicitly specify one. Then we multiply this by a factor # >1 to account for the amount of stray light that is expected. If we do not provide a date # for the observation, then it will assume that it's the vernal equinox (sun at (0,0) in # ecliptic coordinates) in 2025. sky_level = wfirst.getSkyLevel(filters[filter_name], world_pos=SCA_cent_pos) sky_level *= (1.0 + wfirst.stray_light_fraction) # Make a image of the sky that takes into account the spatially variable pixel scale. Note # that makeSkyImage() takes a bit of time. If you do not care about the variable pixel # scale, you could simply compute an approximate sky level in e-/pix by multiplying # sky_level by wfirst.pixel_scale**2, and add that to final_image. sky_image = final_image.copy() wcs.makeSkyImage(sky_image, sky_level) # This image is in units of e-/pix. Finally we add the expected thermal backgrounds in this # band. These are provided in e-/pix/s, so we have to multiply by the exposure time. sky_image += wfirst.thermal_backgrounds[filter_name]*wfirst.exptime # Adding sky level to the image. final_image += sky_image # Now that all sources of signal (from astronomical objects and background) have been added # to the image, we can start adding noise and detector effects. There is a utility, # galsim.wfirst.allDetectorEffects(), that can apply ALL implemented noise and detector # effects in the proper order. Here we step through the process and explain these in a bit # more detail without using that utility. # First, we include the expected Poisson noise: final_image.addNoise(poisson_noise) # The subsequent steps account for the non-ideality of the detectors. # 1) Reciprocity failure: # Reciprocity, in the context of photography, is the inverse relationship between the # incident flux (I) of a source object and the exposure time (t) required to produce a given # response(p) in the detector, i.e., p = I*t. However, in NIR detectors, this relation does # not hold always. The pixel response to a high flux is larger than its response to a low # flux. This flux-dependent non-linearity is known as 'reciprocity failure', and the # approximate amount of reciprocity failure for the WFIRST detectors is known, so we can # include this detector effect in our images. if diff_mode: # Save the image before applying the transformation to see the difference save_image = final_image.copy() # If we had wanted to, we could have specified a different exposure time than the default # one for WFIRST, but otherwise the following routine does not take any arguments. wfirst.addReciprocityFailure(final_image) logger.debug('Included reciprocity failure in {0}-band image'.format(filter_name)) if diff_mode: # Isolate the changes due to reciprocity failure. diff = final_image-save_image out_filename = os.path.join(outpath,'demo13_RecipFail_{0}.fits'.format(filter_name)) final_image.write(out_filename) out_filename = os.path.join(outpath, 'demo13_diff_RecipFail_{0}.fits'.format(filter_name)) diff.write(out_filename) # At this point in the image generation process, an integer number of photons gets # detected, hence we have to round the pixel values to integers: final_image.quantize() # 2) Adding dark current to the image: # Even when the detector is unexposed to any radiation, the electron-hole pairs that # are generated within the depletion region due to finite temperature are swept by the # high electric field at the junction of the photodiode. This small reverse bias # leakage current is referred to as 'dark current'. It is specified by the average # number of electrons reaching the detectors per unit time and has an associated # Poisson noise since it is a random event. dark_current = wfirst.dark_current*wfirst.exptime dark_noise = galsim.DeviateNoise(galsim.PoissonDeviate(rng, dark_current)) final_image.addNoise(dark_noise) # NOTE: Sky level and dark current might appear like a constant background that can be # simply subtracted. However, these contribute to the shot noise and matter for the # non-linear effects that follow. Hence, these must be included at this stage of the # image generation process. We subtract these backgrounds in the end. # 3) Applying a quadratic non-linearity: # In order to convert the units from electrons to ADU, we must use the gain factor. The gain # has a weak dependency on the charge present in each pixel. This dependency is accounted # for by changing the pixel values (in electrons) and applying a constant nominal gain # later, which is unity in our demo. # Save the image before applying the transformation to see the difference: if diff_mode: save_image = final_image.copy() # Apply the WFIRST nonlinearity routine, which knows all about the nonlinearity expected in # the WFIRST detectors. wfirst.applyNonlinearity(final_image) # Note that users who wish to apply some other nonlinearity function (perhaps for other NIR # detectors, or for CCDs) can use the more general nonlinearity routine, which uses the # following syntax: # final_image.applyNonlinearity(NLfunc=NLfunc) # with NLfunc being a callable function that specifies how the output image pixel values # should relate to the input ones. logger.debug('Applied nonlinearity to {0}-band image'.format(filter_name)) if diff_mode: diff = final_image-save_image out_filename = os.path.join(outpath,'demo13_NL_{0}.fits'.format(filter_name)) final_image.write(out_filename) out_filename = os.path.join(outpath,'demo13_diff_NL_{0}.fits'.format(filter_name)) diff.write(out_filename) # Save this image to do the diff after applying IPC. save_image = final_image.copy() # 4) Including Interpixel capacitance: # The voltage read at a given pixel location is influenced by the charges present in the # neighboring pixel locations due to capacitive coupling of sense nodes. This interpixel # capacitance effect is modeled as a linear effect that is described as a convolution of a # 3x3 kernel with the image. The WFIRST IPC routine knows about the kernel already, so the # user does not have to supply it. wfirst.applyIPC(final_image) logger.debug('Applied interpixel capacitance to {0}-band image'.format(filter_name)) if diff_mode: # Isolate the changes due to the interpixel capacitance effect. diff = final_image-save_image out_filename = os.path.join(outpath,'demo13_IPC_{0}.fits'.format(filter_name)) final_image.write(out_filename) out_filename = os.path.join(outpath,'demo13_diff_IPC_{0}.fits'.format(filter_name)) diff.write(out_filename) # 5) Adding read noise: # Read noise is the noise due to the on-chip amplifier that converts the charge into an # analog voltage. We already applied the Poisson noise due to the sky level, so read noise # should just be added as Gaussian noise: read_noise = galsim.GaussianNoise(rng, sigma=wfirst.read_noise) final_image.addNoise(read_noise) logger.debug('Added readnoise to {0}-band image'.format(filter_name)) # We divide by the gain to convert from e- to ADU. Currently, the gain value in the WFIRST # module is just set to 1, since we don't know what the exact gain will be, although it is # expected to be approximately 1. Eventually, this may change when the camera is assembled, # and there may be a different value for each SCA. For now, there is just a single number, # which is equal to 1. final_image /= wfirst.gain # Finally, the analog-to-digital converter reads in an integer value. final_image.quantize() # Note that the image type after this step is still a float. If we want to actually # get integer values, we can do new_img = galsim.Image(final_image, dtype=int) # Since many people are used to viewing background-subtracted images, we provide a # version with the background subtracted (also rounding that to an int). sky_image.quantize() tot_sky_image = (sky_image + round(dark_current))/wfirst.gain tot_sky_image.quantize() final_image -= tot_sky_image logger.debug('Subtracted background for {0}-band image'.format(filter_name)) # Write the final image to a file. out_filename = os.path.join(outpath,'demo13_{0}.fits'.format(filter_name)) final_image.write(out_filename) logger.info('Completed {0}-band image.'.format(filter_name)) logger.info('You can display the output in ds9 with a command line that looks something like:') logger.info('ds9 -zoom 0.5 -scale limits -500 1000 -rgb '+ '-red output/demo13_H158.fits '+ '-green output/demo13_J129.fits '+ '-blue output/demo13_Y106.fits')
def test_poisson_noise(): """Test Poisson random number generator """ pMean = 17 p = galsim.PoissonDeviate(testseed, mean=pMean) pResult = np.empty((10, 10)) p.generate(pResult) noise = galsim.DeviateNoise(p) # Test filling an image noise.rng.seed(testseed) testimage = galsim.ImageI(10, 10) testimage.addNoise(galsim.DeviateNoise(p)) np.testing.assert_array_equal( testimage.array, pResult, err_msg= 'Wrong poisson random number sequence generated when applied to image.' ) # The PoissonNoise version also subtracts off the mean value pn = galsim.PoissonNoise(galsim.BaseDeviate(testseed), sky_level=pMean) testimage.fill(0) testimage.addNoise(pn) np.testing.assert_array_equal( testimage.array, pResult - pMean, err_msg= 'Wrong poisson random number sequence generated using PoissonNoise') # Test filling a single-precision image pn.rng.seed(testseed) testimage = galsim.ImageF(10, 10) testimage.addNoise(pn) np.testing.assert_array_almost_equal( testimage.array, pResult - pMean, precisionF, err_msg= 'Wrong Poisson random number sequence generated when applied to ImageF.' ) # Test filling an image with Fortran ordering pn.rng.seed(testseed) testimage = galsim.ImageD(10, 10) testimage.addNoise(pn) np.testing.assert_array_almost_equal( testimage.array, pResult - pMean, err_msg="Wrong Poisson noise generated for Fortran-ordered Image") # Check PoissonNoise variance: np.testing.assert_almost_equal( pn.getVariance(), pMean, precision, err_msg="PoissonNoise getVariance returns wrong variance") np.testing.assert_almost_equal( pn.sky_level, pMean, precision, err_msg="PoissonNoise sky_level returns wrong value") # Check that the noise model really does produce this variance. big_im = galsim.Image(2048, 2048, dtype=float) big_im.addNoise(pn) var = np.var(big_im.array) print('variance = ', var) print('getVar = ', pn.getVariance()) np.testing.assert_almost_equal( var, pn.getVariance(), 1, err_msg='Realized variance for PoissonNoise did not match getVariance()' ) # Check that PoissonNoise adds to the image, not overwrites the image. gal = galsim.Exponential(half_light_radius=2.3, flux=0.3) # Note: in this case, flux/size^2 needs to be << sky_level or it will mess up the statistics. gal.drawImage(image=big_im) big_im.addNoise(pn) gal.withFlux(-0.3).drawImage(image=big_im, add_to_image=True) var = np.var(big_im.array) np.testing.assert_almost_equal( var, pn.getVariance(), 1, err_msg='PoissonNoise wrong when already an object drawn on the image') # Check withVariance pn = pn.withVariance(9.) np.testing.assert_almost_equal( pn.getVariance(), 9., precision, err_msg="PoissonNoise withVariance results in wrong variance") np.testing.assert_almost_equal( pn.sky_level, 9., precision, err_msg="PoissonNoise withVariance results in wrong sky_level") # Check withScaledVariance pn = pn.withScaledVariance(4.) np.testing.assert_almost_equal( pn.getVariance(), 36, precision, err_msg="PoissonNoise withScaledVariance results in wrong variance") np.testing.assert_almost_equal( pn.sky_level, 36., precision, err_msg="PoissonNoise withScaledVariance results in wrong sky_level") # Check arithmetic pn = pn.withVariance(0.5) pn2 = pn * 3 np.testing.assert_almost_equal( pn2.getVariance(), 1.5, precision, err_msg="PoissonNoise pn*3 results in wrong variance") np.testing.assert_almost_equal( pn.getVariance(), 0.5, precision, err_msg="PoissonNoise pn*3 results in wrong variance for original pn") pn2 = 5 * pn np.testing.assert_almost_equal( pn2.getVariance(), 2.5, precision, err_msg="PoissonNoise 5*pn results in wrong variance") np.testing.assert_almost_equal( pn.getVariance(), 0.5, precision, err_msg="PoissonNoise 5*pn results in wrong variance for original pn") pn2 = pn / 2 np.testing.assert_almost_equal( pn2.getVariance(), 0.25, precision, err_msg="PoissonNoise pn/2 results in wrong variance") np.testing.assert_almost_equal( pn.getVariance(), 0.5, precision, err_msg="PoissonNoise 5*pn results in wrong variance for original pn") pn *= 3 np.testing.assert_almost_equal( pn.getVariance(), 1.5, precision, err_msg="PoissonNoise pn*=3 results in wrong variance") pn /= 2 np.testing.assert_almost_equal( pn.getVariance(), 0.75, precision, err_msg="PoissonNoise pn/=2 results in wrong variance") # Check starting with PoissonNoise() pn = galsim.PoissonNoise() pn = pn.withVariance(9.) np.testing.assert_almost_equal( pn.getVariance(), 9., precision, err_msg="PoissonNoise().withVariance results in wrong variance") np.testing.assert_almost_equal( pn.sky_level, 9., precision, err_msg="PoissonNoise().withVariance results in wrong sky_level") pn = pn.withScaledVariance(4.) np.testing.assert_almost_equal( pn.getVariance(), 36, precision, err_msg="PoissonNoise().withScaledVariance results in wrong variance") np.testing.assert_almost_equal( pn.sky_level, 36., precision, err_msg="PoissonNoise().withScaledVariance results in wrong sky_level") # Check picklability do_pickle(pn, lambda x: (x.rng.serialize(), x.sky_level)) do_pickle(pn, drawNoise) do_pickle(pn) # Check copy, eq and ne pn = pn.withVariance(pMean) pn2 = galsim.PoissonNoise(pn.rng.duplicate(), pMean) pn3 = pn.copy() pn4 = pn.copy(rng=galsim.BaseDeviate(11)) pn5 = galsim.PoissonNoise(pn.rng, 2 * pMean) assert pn == pn2 assert pn == pn3 assert pn != pn4 assert pn != pn5 assert pn.rng.raw() == pn2.rng.raw() assert pn == pn2 assert pn == pn3 pn.rng.raw() assert pn != pn2 assert pn == pn3