def processStamp(self, obj_num, config, base, logger): # Note: This is just a placeholder for now. Once we implement defects, saturation, etc., # these features should be marked in the badpix mask. For now though, all pixels = 0. if base['do_noise_in_stamps']: badpix_im = galsim.ImageS(base['current_stamp'].bounds, wcs=base['wcs'], init_value=0) self.scratch[obj_num] = badpix_im
def processImage(self, index, obj_nums, config, base, logger): image = galsim.ImageS(base['image_bounds'], wcs=base['wcs'], init_value=0) if len(self.scratch) > 0.: # If we have been accumulating the variance on the stamps, build the total from them. # Make sure to only use the stamps for objects in this image. for obj_num in obj_nums: stamp = self.scratch[obj_num] b = stamp.bounds & image.bounds if b.isDefined(): # pragma: no branch # This next line is equivalent to: # image[b] |= stamp[b] # except that this doesn't work through the proxy. We can only call methods # that don't start with _. Hence using the more verbose form here. image.setSubImage(b, image.subImage(b) | stamp[b]) else: # Otherwise, build the bad pixel mask here. # Again, nothing here yet. pass self.data[index] = image
new_hdu_list = pyfits.HDUList() # First is the image hdu. We use the new image that we built. assert se_hdu==1 if do_print: print 'im = ',im if do_print: print im.array im.write(hdu_list=new_hdu_list) if do_print: print 'after write im: ',new_hdu_list if do_print: print new_hdu_list[0].header if do_print: print new_hdu_list[0].data # Leave the badpix image the same. # TODO: It might be nice to add in artifacts in the image based on the bad pixel map. assert se_badpix_hdu==2 badpix_im = galsim.fits.read(hdu_list=hdu_list[se_badpix_hdu], compression='rice') badpix_im = galsim.ImageS(badpix_im) if do_print: print 'badpix_im = ',badpix_im if do_print: print badpix_im.array if do_print: print badpix_im.array.astype(numpy.int32) if do_print: print 'max = ',badpix_im.array.max() badpix_im.write(hdu_list=new_hdu_list) if do_print: print 'after write badpix_im: ',new_hdu_list if do_print: print new_hdu_list[1].header if do_print: print new_hdu_list[1].data # Rescale the weight image to have the correct mean noise level. We still let the # weight map be variable, but the nosie we add is constant. (We can change this is # we want using galsim.VariableGaussianNoise.) However, it was requested that the # mean level be accurate. So we rescale the map to have the right mean. assert se_wt_hdu==3 wt_im = galsim.fits.read(hdu_list=hdu_list[se_wt_hdu], compression='rice')
def BuildScatteredImage(config, logger=None, image_num=0, obj_num=0, make_psf_image=False, make_weight_image=False, make_badpix_image=False): """ Build an image containing multiple objects placed at arbitrary locations. @param config A configuration dict. @param logger If given, a logger object to log progress. @param image_num If given, the current image_num (default = 0) @param obj_num If given, the current obj_num (default = 0) @param make_psf_image Whether to make psf_image. @param make_weight_image Whether to make weight_image. @param make_badpix_image Whether to make badpix_image. @return (image, psf_image, weight_image, badpix_image) Note: All 4 images are always returned in the return tuple, but the latter 3 might be None depending on the parameters make_*_image. """ config['seq_index'] = image_num ignore = [ 'random_seed', 'draw_method', 'noise', 'wcs', 'nproc' , 'image_pos', 'sky_pos', 'n_photons', 'wmult', 'offset', 'stamp_size', 'stamp_xsize', 'stamp_ysize', 'gsparams' ] req = { 'nobjects' : int } opt = { 'size' : int , 'xsize' : int , 'ysize' : int , 'pixel_scale' : float , 'nproc' : int , 'index_convention' : str, 'sky_level' : float , 'sky_level_pixel' : float } params = galsim.config.GetAllParams( config['image'], 'image', config, req=req, opt=opt, ignore=ignore)[0] nobjects = params['nobjects'] # Special check for the size. Either size or both xsize and ysize is required. if 'size' not in params: if 'xsize' not in params or 'ysize' not in params: raise AttributeError( "Either attribute size or both xsize and ysize required for image.type=Scattered") full_xsize = params['xsize'] full_ysize = params['ysize'] else: if 'xsize' in params: raise AttributeError( "Attributes xsize is invalid if size is set for image.type=Scattered") if 'ysize' in params: raise AttributeError( "Attributes ysize is invalid if size is set for image.type=Scattered") full_xsize = params['size'] full_ysize = params['size'] pixel_scale = params.get('pixel_scale',1.0) config['pixel_scale'] = pixel_scale convention = params.get('index_convention','1') _set_image_origin(config,convention) 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) sky_level_pixel = params.get('sky_level_pixel',None) if 'sky_level' in params: sky_level_pixel = params['sky_level'] * pixel_scale**2 # If image_xsize and image_ysize were set in config, make sure it matches. if ( 'image_xsize' in config and 'image_ysize' in config and (full_xsize != config['image_xsize'] or full_ysize != config['image_ysize']) ): raise ValueError( "Unable to reconcile saved image_xsize and image_ysize with provided "+ "xsize=%d, ysize=%d, "%(full_xsize,full_ysize)) if 'pix' not in config: config['pix'] = { 'type' : 'Pixel' , 'xw' : pixel_scale } # Set the rng to use for image stuff. if 'random_seed' in config['image']: #print 'random_seed = ',config['image']['random_seed'] config['seq_index'] = obj_num+nobjects #print 'seq_index = ',config['seq_index'] # Technically obj_num+nobjects will be the index of the random seed used for the next # image's first object (if there is a next image). But I don't think that will have # any adverse effects. seed = galsim.config.ParseValue(config['image'], 'random_seed', config, int)[0] #print 'seed = ',seed rng = galsim.BaseDeviate(seed) else: rng = galsim.BaseDeviate() # If we have a power spectrum in config, we need to get a new realization at the start # of each image. if 'power_spectrum' in config: input_ps = config['input']['power_spectrum'] if not isinstance(input_ps, list): input_ps = [ input_ps ] for i in range(len(config['power_spectrum'])): # PowerSpectrum can only do a square FFT, so make it the larger of the two sizes. if 'grid_spacing' not in input_ps[i]: raise AttributeError( "power_spectrum.grid_spacing required for image.type=Scattered") grid_dx = galsim.config.ParseValue(input_ps[i], 'grid_spacing', config, float)[0] full_size = max(full_xsize, full_ysize) grid_nx = full_size * pixel_scale / grid_dx + 1 if 'interpolant' in input_ps[i]: interpolant = galsim.config.ParseValue(input_ps[i], 'interpolant', config, str)[0] else: interpolant = None config['power_spectrum'][i].buildGrid(grid_spacing=grid_dx, ngrid=grid_nx, rng=rng, interpolant=interpolant) # We don't care about the output here. This just builds the grid, which we'll # access for each object using its position. if 'image_pos' in config['image'] and 'sky_pos' in config['image']: raise AttributeError("Both image_pos and sky_pos specified for Scattered image.") if 'image_pos' not in config['image'] and 'sky_pos' not in config['image']: xmin = config['image_origin'].x xmax = xmin + full_xsize-1 ymin = config['image_origin'].y ymax = ymin + full_ysize-1 config['image']['image_pos'] = { 'type' : 'XY' , 'x' : { 'type' : 'Random' , 'min' : xmin , 'max' : xmax }, 'y' : { 'type' : 'Random' , 'min' : ymin , 'max' : ymax } } nproc = params.get('nproc',1) full_image = galsim.ImageF(full_xsize, full_ysize, scale=pixel_scale) full_image.setOrigin(config['image_origin']) full_image.setZero() # Also define the overall image center, since we need that to calculate the position # of each stamp relative to the center. config['image_cen'] = full_image.bounds.trueCenter() #print 'image_cen = ',full_image.bounds.trueCenter() if make_psf_image: full_psf_image = galsim.ImageF(full_xsize, full_ysize, scale=pixel_scale) full_psf_image.setOrigin(config['image_origin']) full_psf_image.setZero() else: full_psf_image = None if make_weight_image: full_weight_image = galsim.ImageF(full_xsize, full_ysize, scale=pixel_scale) full_weight_image.setOrigin(config['image_origin']) full_weight_image.setZero() else: full_weight_image = None if make_badpix_image: full_badpix_image = galsim.ImageS(full_xsize, full_ysize, scale=pixel_scale) full_badpix_image.setOrigin(config['image_origin']) full_badpix_image.setZero() else: full_badpix_image = None stamp_images = galsim.config.BuildStamps( nobjects=nobjects, config=config, nproc=nproc, logger=logger,obj_num=obj_num, sky_level_pixel=sky_level_pixel, do_noise=False, make_psf_image=make_psf_image, make_weight_image=make_weight_image, make_badpix_image=make_badpix_image) images = stamp_images[0] psf_images = stamp_images[1] weight_images = stamp_images[2] badpix_images = stamp_images[3] for k in range(nobjects): bounds = images[k].bounds & full_image.bounds #print 'stamp bounds = ',images[k].bounds #print 'full bounds = ',full_image.bounds #print 'Overlap = ',bounds if bounds.isDefined(): full_image[bounds] += images[k][bounds] if make_psf_image: full_psf_image[bounds] += psf_images[k][bounds] if make_weight_image: full_weight_image[bounds] += weight_images[k][bounds] if make_badpix_image: full_badpix_image[bounds] |= badpix_images[k][bounds] else: if logger: logger.warn( "Object centered at (%d,%d) is entirely off the main image,\n"%( images[k].bounds.center().x, images[k].bounds.center().y) + "whose bounds are (%d,%d,%d,%d)."%( full_image.bounds.xmin, full_image.bounds.xmax, full_image.bounds.ymin, full_image.bounds.ymax)) if 'noise' in config['image']: # Apply the noise to the full image draw_method = galsim.config.GetCurrentValue(config['image'],'draw_method') if draw_method == 'fft': galsim.config.AddNoiseFFT( full_image,full_weight_image,config['image']['noise'],config,rng,sky_level_pixel) elif draw_method == 'phot': galsim.config.AddNoisePhot( full_image,full_weight_image,config['image']['noise'],config,rng,sky_level_pixel) else: raise AttributeError("Unknown draw_method %s."%draw_method) elif sky_level_pixel: # If we aren't doing noise, we still need to add a non-zero sky_level full_image += sky_level_pixel return full_image, full_psf_image, full_weight_image, full_badpix_image
def BuildTiledImage(config, logger=None, image_num=0, obj_num=0, make_psf_image=False, make_weight_image=False, make_badpix_image=False): """ Build an image consisting of a tiled array of postage stamps @param config A configuration dict. @param logger If given, a logger object to log progress. @param image_num If given, the current image_num (default = 0) @param obj_num If given, the current obj_num (default = 0) @param make_psf_image Whether to make psf_image. @param make_weight_image Whether to make weight_image. @param make_badpix_image Whether to make badpix_image. @return (image, psf_image, weight_image, badpix_image) Note: All 4 images are always returned in the return tuple, but the latter 3 might be None depending on the parameters make_*_image. """ config['seq_index'] = image_num ignore = [ 'random_seed', 'draw_method', 'noise', 'wcs', 'nproc' , 'image_pos', 'n_photons', 'wmult', 'offset', 'gsparams' ] req = { 'nx_tiles' : int , 'ny_tiles' : int } opt = { 'stamp_size' : int , 'stamp_xsize' : int , 'stamp_ysize' : int , 'border' : int , 'xborder' : int , 'yborder' : int , 'pixel_scale' : float , 'nproc' : int , 'index_convention' : str, 'sky_level' : float , 'sky_level_pixel' : float , 'order' : str } params = galsim.config.GetAllParams( config['image'], 'image', config, req=req, opt=opt, ignore=ignore)[0] nx_tiles = params['nx_tiles'] ny_tiles = params['ny_tiles'] nobjects = nx_tiles * ny_tiles stamp_size = params.get('stamp_size',0) stamp_xsize = params.get('stamp_xsize',stamp_size) stamp_ysize = params.get('stamp_ysize',stamp_size) convention = params.get('index_convention','1') _set_image_origin(config,convention) if (stamp_xsize == 0) or (stamp_ysize == 0): raise AttributeError( "Both image.stamp_xsize and image.stamp_ysize need to be defined and != 0.") border = params.get("border",0) xborder = params.get("xborder",border) yborder = params.get("yborder",border) pixel_scale = params.get('pixel_scale',1.0) config['pixel_scale'] = pixel_scale 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) sky_level_pixel = params.get('sky_level_pixel',None) if 'sky_level' in params: sky_level_pixel = params['sky_level'] * pixel_scale**2 do_noise = xborder >= 0 and yborder >= 0 # TODO: Note: if one of these is < 0 and the other is > 0, then # this will add noise to the border region. Not exactly the # design, but I didn't bother to do the bookkeeping right to # make the borders pure 0 in that case. full_xsize = (stamp_xsize + xborder) * nx_tiles - xborder full_ysize = (stamp_ysize + yborder) * ny_tiles - yborder # If image_xsize and image_ysize were set in config, make sure it matches. if ( 'image_xsize' in config and 'image_ysize' in config and (full_xsize != config['image_xsize'] or full_ysize != config['image_ysize']) ): raise ValueError( "Unable to reconcile saved image_xsize and image_ysize with provided "+ "nx_tiles=%d, ny_tiles=%d, "%(nx_tiles,ny_tiles) + "xborder=%d, yborder=%d\n"%(xborder,yborder) + "Calculated full_size = (%d,%d) "%(full_xsize,full_ysize)+ "!= required (%d,%d)."%(config['image_xsize'],config['image_ysize'])) if 'pix' not in config: config['pix'] = { 'type' : 'Pixel' , 'xw' : pixel_scale } # Set the rng to use for image stuff. if 'random_seed' in config['image']: config['seq_index'] = obj_num+nobjects # Technically obj_num+nobjects will be the index of the random seed used for the next # image's first object (if there is a next image). But I don't think that will have # any adverse effects. seed = galsim.config.ParseValue(config['image'], 'random_seed', config, int)[0] #print 'seed = ',seed rng = galsim.BaseDeviate(seed) else: rng = galsim.BaseDeviate() # If we have a power spectrum in config, we need to get a new realization at the start # of each image. if 'power_spectrum' in config: input_ps = config['input']['power_spectrum'] if not isinstance(input_ps, list): input_ps = [ input_ps ] for i in range(len(config['power_spectrum'])): # PowerSpectrum can only do a square FFT, so make it the larger of the two n's. n_tiles = max(nx_tiles, ny_tiles) stamp_size = max(stamp_xsize, stamp_ysize) if 'grid_spacing' in input_ps[i]: grid_dx = galsim.config.ParseValue(input_ps[i], 'grid_spacing', config, float)[0] else: grid_dx = stamp_size * pixel_scale if 'interpolant' in input_ps[i]: interpolant = galsim.config.ParseValue(input_ps[i], 'interpolant', config, str)[0] else: interpolant = None config['power_spectrum'][i].buildGrid(grid_spacing=grid_dx, ngrid=n_tiles, rng=rng, interpolant=interpolant) # We don't care about the output here. This just builds the grid, which we'll # access for each object using its position. # Make a list of ix,iy values according to the specified order: order = params.get('order','row').lower() if order.startswith('row'): ix_list = [ ix for iy in range(ny_tiles) for ix in range(nx_tiles) ] iy_list = [ iy for iy in range(ny_tiles) for ix in range(nx_tiles) ] elif order.startswith('col'): ix_list = [ ix for ix in range(nx_tiles) for iy in range(ny_tiles) ] iy_list = [ iy for ix in range(nx_tiles) for iy in range(ny_tiles) ] elif order.startswith('rand'): ix_list = [ ix for ix in range(nx_tiles) for iy in range(ny_tiles) ] iy_list = [ iy for ix in range(nx_tiles) for iy in range(ny_tiles) ] galsim.random.permute(rng, ix_list, iy_list) # Define a 'image_pos' field so the stamps can set their position appropriately in case # we need it for PowerSpectum or NFWHalo. x0 = (stamp_xsize-1)/2. + config['image_origin'].x y0 = (stamp_ysize-1)/2. + config['image_origin'].y dx = stamp_xsize + xborder dy = stamp_ysize + yborder config['image']['image_pos'] = { 'type' : 'XY' , 'x' : { 'type' : 'List', 'items' : [ x0 + ix*dx for ix in ix_list ] }, 'y' : { 'type' : 'List', 'items' : [ y0 + iy*dy for iy in iy_list ] } } nproc = params.get('nproc',1) full_image = galsim.ImageF(full_xsize, full_ysize, scale=pixel_scale) full_image.setOrigin(config['image_origin']) full_image.setZero() # Also define the overall image center, since we need that to calculate the position # of each stamp relative to the center. config['image_cen'] = full_image.bounds.trueCenter() #print 'image_cen = ',full_image.bounds.trueCenter() if make_psf_image: full_psf_image = galsim.ImageF(full_xsize, full_ysize, scale=pixel_scale) full_psf_image.setOrigin(config['image_origin']) full_psf_image.setZero() else: full_psf_image = None if make_weight_image: full_weight_image = galsim.ImageF(full_xsize, full_ysize, scale=pixel_scale) full_weight_image.setOrigin(config['image_origin']) full_weight_image.setZero() else: full_weight_image = None if make_badpix_image: full_badpix_image = galsim.ImageS(full_xsize, full_ysize, scale=pixel_scale) full_badpix_image.setOrigin(config['image_origin']) full_badpix_image.setZero() else: full_badpix_image = None stamp_images = galsim.config.BuildStamps( nobjects=nobjects, config=config, nproc=nproc, logger=logger, obj_num=obj_num, xsize=stamp_xsize, ysize=stamp_ysize, sky_level_pixel=sky_level_pixel, do_noise=do_noise, make_psf_image=make_psf_image, make_weight_image=make_weight_image, make_badpix_image=make_badpix_image) images = stamp_images[0] psf_images = stamp_images[1] weight_images = stamp_images[2] badpix_images = stamp_images[3] for k in range(nobjects): #print 'full bounds = ',full_image.bounds #print 'stamp bounds = ',images[k].bounds assert full_image.bounds.includes(images[k].bounds) b = images[k].bounds full_image[b] += images[k] if make_psf_image: full_psf_image[b] += psf_images[k] if make_weight_image: full_weight_image[b] += weight_images[k] if make_badpix_image: full_badpix_image[b] |= badpix_images[k] if not do_noise: if 'noise' in config['image']: # If we didn't apply noise in each stamp, then we need to apply it now. draw_method = galsim.config.GetCurrentValue(config['image'],'draw_method') if draw_method == 'fft': galsim.config.AddNoiseFFT( full_image,full_weight_image,config['image']['noise'],config,rng, sky_level_pixel) elif draw_method == 'phot': galsim.config.AddNoisePhot( full_image,full_weight_image,config['image']['noise'],config,rng, sky_level_pixel) else: raise AttributeError("Unknown draw_method %s."%draw_method) elif sky_level_pixel: # If we aren't doing noise, we still need to add a non-zero sky_level full_image += sky_level_pixel return full_image, full_psf_image, full_weight_image, full_badpix_image
def build_file(seed, file_name, mass): """A function that does all the work to build a single file. Returns the total time taken. """ t1 = time.time() full_image = galsim.ImageF(image_size, image_size) full_image.setScale(pixel_scale) # The weight image will hold the inverse variance for each pixel. weight_image = galsim.ImageF(image_size, image_size) weight_image.setScale(pixel_scale) # It is common for astrometric images to also have a bad pixel mask. We don't have any # defect simulation currently, so our bad pixel masks are currently all zeros. # But someday, we plan to add defect functionality to GalSim, at which point, we'll # be able to mark those defects on a bad pixel mask. # Note: the S in ImageS means to use "short int" for the data type. # This is a typical choice for a bad pixel image. badpix_image = galsim.ImageS(image_size, image_size) badpix_image.setScale(pixel_scale) # Setup the NFWHalo stuff: nfw = galsim.NFWHalo(mass=mass, conc=nfw_conc, redshift=nfw_z_halo, omega_m=omega_m, omega_lam=omega_lam) # Note: the last two are optional. If they are omitted, then (omega_m=0.3, omega_lam=0.7) # are actually the defaults. If you only specify one of them, the other is set so that # the total is 1. But you can define both values so that the total is not 1 if you want. # Radiation is assumed to be zero and dark energy equation of state w = -1. # If you want to include either radiation or more complicated dark energy models, # you can define your own cosmology class that defines the functions a(z), E(a), and # Da(z_source, z_lens). Then you can pass this to NFWHalo as a `cosmo` parameter. # The "true" center of the image is allowed to be halfway between two pixels, as is the # case for even-sized images. full_image.bounds.center() is an integer position, # which would be 1/2 pixel up and to the right of the true center in this case. im_center = full_image.bounds.trueCenter() for k in range(nobj): # Initialize the random number generator we will be using for this object: rng = galsim.UniformDeviate(seed+k) # Determine where this object is going to go. # We choose points randomly within a donut centered at the center of the main image # in order to avoid placing galaxies too close to the halo center where the lensing # is not weak. We use an inner radius of 10 arcsec and an outer radius of 50 arcsec, # which takes us essentially to the edge of the image. radius = 50 inner_radius = 10 max_rsq = radius**2 min_rsq = inner_radius**2 while True: # (This is essentially a do..while loop.) x = (2.*rng()-1) * radius y = (2.*rng()-1) * radius rsq = x**2 + y**2 if rsq >= min_rsq and rsq <= max_rsq: break pos = galsim.PositionD(x,y) # We also need the position in pixels to determine where to place the postage # stamp on the full image. image_pos = pos / pixel_scale + im_center # For even-sized postage stamps, the nominal center (returned by stamp.bounds.center()) # cannot be at the true center (returned by stamp.bounds.trueCenter()) of the postage # stamp, since the nominal center values have to be integers. Thus, the nominal center # is 1/2 pixel up and to the right of the true center. # If we used odd-sized postage stamps, we wouldn't need to do this. x_nominal = image_pos.x + 0.5 y_nominal = image_pos.y + 0.5 # Get the integer values of these which will be the actual nominal center of the # postage stamp image. ix_nominal = int(math.floor(x_nominal+0.5)) iy_nominal = int(math.floor(y_nominal+0.5)) # The remainder will be accounted for in a shift dx = x_nominal - ix_nominal dy = y_nominal - iy_nominal # Make the pixel: pix = galsim.Pixel(pixel_scale) # Make the PSF profile: psf = galsim.Kolmogorov(fwhm = psf_fwhm) # Determine the random values for the galaxy: flux = rng() * (gal_flux_max-gal_flux_min) + gal_flux_min hlr = rng() * (gal_hlr_max-gal_hlr_min) + gal_hlr_min gd = galsim.GaussianDeviate(rng, sigma = gal_eta_rms) eta1 = gd() # Unlike g or e, large values of eta are valid, so no need to cutoff. eta2 = gd() # Make the galaxy profile with these values: gal = galsim.Exponential(half_light_radius=hlr, flux=flux) gal.applyShear(eta1=eta1, eta2=eta2) # Now apply the appropriate lensing effects for this position from # the NFW halo mass. try: g1,g2 = nfw.getShear( pos , nfw_z_source ) shear = galsim.Shear(g1=g1,g2=g2) except: # This shouldn't happen, since we exclude the inner 10 arcsec, but it's a # good idea to use the try/except block here anyway. import warnings warnings.warn("Warning: NFWHalo shear is invalid -- probably strong lensing! " + "Using shear = 0.") shear = galsim.Shear(g1=0,g2=0) mu = nfw.getMagnification( pos , nfw_z_source ) if mu < 0: import warnings warnings.warn("Warning: mu < 0 means strong lensing! Using mu=25.") mu = 25 elif mu > 25: import warnings warnings.warn("Warning: mu > 25 means strong lensing! Using mu=25.") mu = 25 gal.applyMagnification(mu) gal.applyShear(shear) # Build the final object final = galsim.Convolve([psf, pix, gal]) # Account for the non-integral portion of the position final.applyShift(dx*pixel_scale,dy*pixel_scale) # Draw the stamp image stamp = final.draw(dx=pixel_scale) # Recenter the stamp at the desired position: stamp.setCenter(ix_nominal,iy_nominal) # Find overlapping bounds bounds = stamp.bounds & full_image.bounds full_image[bounds] += stamp[bounds] # Add Poisson noise to the full image sky_level_pixel = sky_level * pixel_scale**2 # Going to the next seed isn't really required, but it matches the behavior of the # config parser, so doing this will result in identical output files. # If you didn't care about that, you could instead construct this as a continuation # of the last RNG from the above loop rng = galsim.BaseDeviate(seed+nobj) full_image.addNoise(galsim.PoissonNoise(rng,sky_level=sky_level_pixel)) # For the weight image, we only want the noise from the sky. (If we were including # read_noise, we'd want that as well.) Including the Poisson noise from the objects # as well tends to bias fits that use this as a weight, since the model becomes # magnitude-dependent. # The variance is just sky_level_pixel. And we want the inverse of this. weight_image.fill(1./sky_level_pixel) # Write the file to disk: galsim.fits.writeMulti([full_image, badpix_image, weight_image], file_name) t2 = time.time() return t2-t1
def measure_shapes(xlist, ylist, file_name, wcs, noweight): """Given x,y positions, an image file, and the wcs, measure shapes and sizes. We use the HSM module from GalSim to do this. Returns e1, e2, size, flag. """ #im = galsim.fits.read(file_name) #bp_im = galsim.fits.read(file_name, hdu=2) #wt_im = galsim.fits.read(file_name, hdu=3) print 'file_name = ', file_name with pyfits.open(file_name) as f: #print 'f = ',f #print 'len(f) = ',len(f) # Some DES images have bad cards. Fix them with verify('fix') before sending to GalSim. for i in range(1, len(f)): f[i].verify('fix') #print 'f[i] = ',f[i] #print 'after verify, f = ',f if file_name.endswith('fz'): im = galsim.fits.read(hdu_list=f, hdu=1, compression='rice') if noweight: bp_im = wt_im = None else: bp_im = galsim.fits.read(hdu_list=f, hdu=2, compression='rice') wt_im = galsim.fits.read(hdu_list=f, hdu=3, compression='rice') else: im = galsim.fits.read(hdu_list=f, hdu=0) if noweight: bp_im = wt_im = None else: bp_im = galsim.fits.read(hdu_list=f, hdu=1) wt_im = galsim.fits.read(hdu_list=f, hdu=2) #print 'im = ',im if not noweight: # The badpix image is offset by 32768 from the true value. Subtract it off. if numpy.any(bp_im.array > 32767): bp_im -= 32768 # Also, convert to int16, since it isn't by default weirdly. I think this is # an error in astropy's RICE algorith, since fitsio converts it correctly to uint16. bp_im = galsim.ImageS(bp_im) # Also, it seems that the weight image has negative values where it should be 0. # Make them 0. wt_im.array[wt_im.array < 0] = 0. # Read the background image as well. base_file = file_name if os.path.splitext(base_file)[1] == '.fz': base_file = os.path.splitext(base_file)[0] if os.path.splitext(base_file)[1] == '.fits': base_file = os.path.splitext(base_file)[0] bkg_file_name = base_file + '_bkg.fits.fz' print 'bkg_file_name = ', bkg_file_name if os.path.exists(bkg_file_name): #bkg_im = galsim.fits.read(bkg_file_name) with pyfits.open(bkg_file_name) as f: f[1].verify('fix') bkg_im = galsim.fits.read(hdu_list=f, hdu=1, compression='rice') im -= bkg_im # Subtract off the sky background. else: cat_file_name = base_file + '_psfcat.fits' print cat_file_name if os.path.exists(cat_file_name): print 'use BACKGROUND from ', cat_file_name with pyfits.open(cat_file_name) as f: bkg = f[2].data['BACKGROUND'] print 'bkg = ', bkg bkg = numpy.median(bkg) print 'median = ', bkg im -= bkg else: print 'No easy way to estimate background. Assuming image is zero subtracted...' stamp_size = 48 n_psf = len(xlist) e1_list = [999.] * n_psf e2_list = [999.] * n_psf s_list = [999.] * n_psf flag_list = [0] * n_psf print 'len(xlist) = ', len(xlist) for i in range(n_psf): x = xlist[i] y = ylist[i] print 'Measure shape for star at ', x, y b = galsim.BoundsI( int(x) - stamp_size / 2, int(x) + stamp_size / 2, int(y) - stamp_size / 2, int(y) + stamp_size / 2) b = b & im.bounds try: subim = im[b] if noweight: subbp = subwt = None else: subbp = bp_im[b] subwt = wt_im[b] #print 'subim = ',subim.array #print 'subwt = ',subwt.array #print 'subbp = ',subbp.array #shape_data = subim.FindAdaptiveMom(weight=subwt, badpix=subbp, strict=False) shape_data = subim.FindAdaptiveMom(weight=subwt, strict=False) except Exception as e: print 'Caught ', e print ' *** Bad measurement (caught exception). Mask this one.' flag_list[i] = MEAS_BAD_MEASUREMENT continue #print 'shape_data = ',shape_data #print 'image_bounds = ',shape_data.image_bounds #print 'shape = ',shape_data.observed_shape #print 'sigma = ',shape_data.moments_sigma #print 'amp = ',shape_data.moments_amp #print 'centroid = ',shape_data.moments_centroid #print 'rho4 = ',shape_data.moments_rho4 #print 'niter = ',shape_data.moments_n_iter if shape_data.moments_status != 0: print 'status = ', shape_data.moments_status print ' *** Bad measurement. Mask this one.' flag_list[i] = MEAS_BAD_MEASUREMENT continue dx = shape_data.moments_centroid.x - x dy = shape_data.moments_centroid.y - y #print 'dcentroid = ',dx,dy if dx**2 + dy**2 > MAX_CENTROID_SHIFT**2: print ' *** Centroid shifted by ', dx, dy, '. Mask this one.' flag_list[i] = MEAS_CENTROID_SHIFT continue e1 = shape_data.observed_shape.e1 e2 = shape_data.observed_shape.e2 s = shape_data.moments_sigma # Note: this is (det M)^1/4, not ((Ixx+Iyy)/2)^1/2. # For reference, the latter is size * (1-e^2)^-1/4 # So, not all that different, especially for stars with e ~= 0. # Account for the WCS: #print 'wcs = ',wcs jac = wcs.jacobian(galsim.PositionD(x, y)) #print 'jac = ',jac # ( Iuu Iuv ) = ( dudx dudy ) ( Ixx Ixy ) ( dudx dvdx ) # ( Iuv Ivv ) ( dvdx dvdy ) ( Ixy Iyy ) ( dudy dvdy ) M = numpy.matrix([[1 + e1, e2], [e2, 1 - e1]]) #print 'M = ',M #print 'det(M) = ',numpy.linalg.det(M) M2 = jac.getMatrix() * M * jac.getMatrix().T #print 'M2 = ',M2 #print 'det(M2) = ',numpy.linalg.det(M2) e1 = (M2[0, 0] - M2[1, 1]) / (M2[0, 0] + M2[1, 1]) e2 = (2. * M2[0, 1]) / (M2[0, 0] + M2[1, 1]) #print 's = ',s s *= abs(numpy.linalg.det(jac.getMatrix()))**0.5 #print 's -> ',s # Now convert back to a more normal shear definition, rather than distortion. shear = galsim.Shear(e1=e1, e2=e2) e1_list[i] = shear.g1 e2_list[i] = shear.g2 s_list[i] = s return e1_list, e2_list, s_list, flag_list
def readImages(self, logger=None): """Read in the images from the input files and return them. :param logger: A logger object for logging debug info. [default: None] :returns: a list of galsim.Image instances """ import galsim # Read in the images from the files self.images = [] for fname in self.image_files: if logger: logger.warning("Reading image file %s",fname) self.images.append(galsim.fits.read(fname, hdu=self.image_hdu)) # Either read in the weight image, or build a dummy one if len(self.images) == 1: plural = '' else: plural = 's' if self.weight_hdu is not None: if logger: logger.info("Reading weight image%s from hdu %d.", plural, self.weight_hdu) self.weight = [ galsim.fits.read(fname, hdu=self.weight_hdu) for fname in self.image_files ] for wt in self.weight: if np.all(wt.array == 0): logger.error("According to the weight mask in %s, all pixels have zero weight!", fname) elif self.noise is not None: if logger: logger.debug("Making uniform weight image%s based on noise variance = %f", plural, self.noise) self.weight = [ galsim.ImageF(im.bounds, init_value=1./self.noise) for im in self.images ] else: if logger: logger.debug("Making trivial (wt==1) weight image%s", plural) self.weight = [ galsim.ImageF(im.bounds, init_value=1) for im in self.images ] # If requested, set wt=0 for any bad pixels if self.badpix_hdu is not None: if logger: logger.info("Reading badpix image%s from hdu %d.", plural, self.badpix_hdu) for fname, wt in zip(self.image_files, self.weight): badpix = galsim.fits.read(fname, hdu=self.badpix_hdu) # The badpix image may be offset by 32768 from the true value. # If so, subtract it off. if np.any(badpix.array > 32767): if logger: logger.debug('min(badpix) = %s',np.min(badpix.array)) logger.debug('max(badpix) = %s',np.max(badpix.array)) logger.debug("subtracting 32768 from all values in badpix image") badpix -= 32768 if np.any(badpix.array < -32767): if logger: logger.debug('min(badpix) = %s',np.min(badpix.array)) logger.debug('max(badpix) = %s',np.max(badpix.array)) logger.debug("adding 32768 to all values in badpix image") badpix += 32768 # Also, convert to int16, in case it isn't by default. badpix = galsim.ImageS(badpix) if np.all(badpix.array != 0): logger.error("According to the bad pixel array in %s, all pixels are masked!", fname) wt.array[badpix.array != 0] = 0
def BuildSingleStamp(config, xsize=0, ysize=0, obj_num=0, do_noise=True, logger=None, make_psf_image=False, make_weight_image=False, make_badpix_image=False): """ Build a single image using the given config file @param config A configuration dict. @param xsize The xsize of the image to build (if known). [default: 0] @param ysize The ysize of the image to build (if known). [default: 0] @param obj_num If given, the current obj_num [default: 0] @param do_noise Whether to add noise to the image (according to config['noise']). [default: True] @param logger If given, a logger object to log progress. [default: None] @param make_psf_image Whether to make psf_image. [default: False] @param make_weight_image Whether to make weight_image. [default: False] @param make_badpix_image Whether to make badpix_image. [default: False] @returns the tuple (image, psf_image, weight_image, badpix_image, current_var, time) """ import time t1 = time.time() # For everything except random_seed, the default key is obj_num_in_file config['index_key'] = 'obj_num_in_file' config['obj_num'] = obj_num # Initialize the random number generator we will be using. if 'random_seed' in config['image']: config['index_key'] = 'obj_num' seed = galsim.config.ParseValue(config['image'], 'random_seed', config, int)[0] config['index_key'] = 'obj_num_in_file' if logger: logger.debug('obj %d: seed = %d', obj_num, seed) rng = galsim.BaseDeviate(seed) else: rng = galsim.BaseDeviate() # Store the rng in the config for use by BuildGSObject function. config['rng'] = rng if 'gd' in config: del config['gd'] # In case it was set. if 'image' in config and 'retry_failures' in config['image']: ntries = galsim.config.ParseValue(config['image'], 'retry_failures', config, int)[0] # This is how many _re_-tries. Do at least 1, so ntries is 1 more than this. ntries = ntries + 1 else: ntries = 1 for itry in range(ntries): try: # The rest of the stamp generation stage is wrapped in a try/except block. # If we catch an exception, we continue the for loop to try again. # On the last time through, we reraise any exception caught. # If no exception is thrown, we simply break the loop and return. # Determine the size of this stamp if not xsize: if 'stamp_xsize' in config['image']: xsize = galsim.config.ParseValue(config['image'], 'stamp_xsize', config, int)[0] elif 'stamp_size' in config['image']: xsize = galsim.config.ParseValue(config['image'], 'stamp_size', config, int)[0] if not ysize: if 'stamp_ysize' in config['image']: ysize = galsim.config.ParseValue(config['image'], 'stamp_ysize', config, int)[0] elif 'stamp_size' in config['image']: ysize = galsim.config.ParseValue(config['image'], 'stamp_size', config, int)[0] if False: logger.debug('obj %d: xsize,ysize = %d,%d', obj_num, xsize, ysize) if xsize: config['stamp_xsize'] = xsize if ysize: config['stamp_ysize'] = ysize # Determine where this object is going to go: if 'image_pos' in config['image'] and 'world_pos' in config[ 'image']: image_pos = galsim.config.ParseValue(config['image'], 'image_pos', config, galsim.PositionD)[0] world_pos = galsim.config.ParseValue(config['image'], 'world_pos', config, galsim.PositionD)[0] elif 'image_pos' in config['image']: image_pos = galsim.config.ParseValue(config['image'], 'image_pos', config, galsim.PositionD)[0] # Calculate and save the position relative to the image center world_pos = config['wcs'].toWorld(image_pos) # Wherever we use the world position, we expect a Euclidean position, not a # CelestialCoord. So if it is the latter, project it onto a tangent plane at the # image center. if isinstance(world_pos, galsim.CelestialCoord): # Then project this position relative to the image center. world_center = config['wcs'].toWorld( config['image_center']) world_pos = world_center.project(world_pos, projection='gnomonic') elif 'world_pos' in config['image']: world_pos = galsim.config.ParseValue(config['image'], 'world_pos', config, galsim.PositionD)[0] # Calculate and save the position relative to the image center image_pos = config['wcs'].toImage(world_pos) else: image_pos = None world_pos = None # Save these values for possible use in Evals or other modules if image_pos is not None: config['image_pos'] = image_pos if logger: logger.debug('obj %d: image_pos = %s', obj_num, str(config['image_pos'])) if world_pos is not None: config['world_pos'] = world_pos if logger: logger.debug('obj %d: world_pos = %s', obj_num, str(config['world_pos'])) if image_pos is not None: import math # The image_pos refers to the location of the true center of the image, which is # not necessarily the nominal center we need for adding to the final image. In # particular, even-sized images have their nominal center offset by 1/2 pixel up # and to the right. # N.B. This works even if xsize,ysize == 0, since the auto-sizing always produces # even sized images. nominal_x = image_pos.x # Make sure we don't change image_pos, which is nominal_y = image_pos.y # stored in config['image_pos']. if xsize % 2 == 0: nominal_x += 0.5 if ysize % 2 == 0: nominal_y += 0.5 if False: logger.debug('obj %d: nominal pos = %f,%f', obj_num, nominal_x, nominal_y) icenter = galsim.PositionI(int(math.floor(nominal_x + 0.5)), int(math.floor(nominal_y + 0.5))) if False: logger.debug('obj %d: nominal icenter = %s', obj_num, str(icenter)) offset = galsim.PositionD(nominal_x - icenter.x, nominal_y - icenter.y) if False: logger.debug('obj %d: offset = %s', obj_num, str(offset)) else: icenter = None offset = galsim.PositionD(0., 0.) # Set the image_pos to (0,0) in case the wcs needs it. Probably, if # there is no image_pos or world_pos defined, then it is unlikely a # non-trivial wcs will have been set. So anything would actually be fine. config['image_pos'] = galsim.PositionD(0., 0.) if False: logger.debug('obj %d: no offset', obj_num) gsparams = {} if 'gsparams' in config['image']: gsparams = galsim.config.UpdateGSParams( gsparams, config['image']['gsparams'], 'gsparams', config) skip = False try: t4 = t3 = t2 = t1 # in case we throw. psf = BuildPSF(config, logger, gsparams) t2 = time.time() gal = BuildGal(config, logger, gsparams) t4 = time.time() # Check that we have at least gal or psf. if not (gal or psf): raise AttributeError( "At least one of gal or psf must be specified in config." ) except galsim.config.gsobject.SkipThisObject, e: if logger: logger.debug('obj %d: Caught SkipThisObject: e = %s', obj_num, e.msg) if e.msg: # If there is a message, upgrade to info level logger.info('Skipping object %d: %s', obj_num, e.msg) skip = True if not skip and 'offset' in config['image']: offset1 = galsim.config.ParseValue(config['image'], 'offset', config, galsim.PositionD)[0] offset += offset1 if 'image' in config and 'draw_method' in config['image']: method = galsim.config.ParseValue(config['image'], 'draw_method', config, str)[0] else: method = 'auto' if method not in [ 'auto', 'fft', 'phot', 'real_space', 'no_pixel', 'sb' ]: raise AttributeError("Invalid draw_method: %s" % method) if skip: if xsize and ysize: # If the size is set, we need to do something reasonable to return this size. im = galsim.ImageF(xsize, ysize) im.setOrigin(config['image_origin']) im.setZero() if do_noise: galsim.config.AddNoise(config, 'skip', im, weight_im, current_var, logger) else: # Otherwise, we don't set the bounds, so it will be noticed as invalid upstream. im = galsim.ImageF() if make_weight_image: weight_im = galsim.ImageF(im.bounds, wcs=im.wcs) weight_im.setZero() else: weight_im = None current_var = 0 else: im, current_var = DrawStamp(psf, gal, config, xsize, ysize, offset, method) if icenter: im.setCenter(icenter.x, icenter.y) if make_weight_image: weight_im = galsim.ImageF(im.bounds, wcs=im.wcs) weight_im.setZero() else: weight_im = None if do_noise: galsim.config.AddNoise(config, method, im, weight_im, current_var, logger) if make_badpix_image: badpix_im = galsim.ImageS(im.bounds, wcs=im.wcs) badpix_im.setZero() else: badpix_im = None t5 = time.time() if make_psf_image: psf_im = DrawPSFStamp(psf, config, im.bounds, offset, method) if ('output' in config and 'psf' in config['output'] and 'signal_to_noise' in config['output']['psf'] and 'noise' in config['image']): galsim.config.AddNoise(config, 'fft', psf_im, None, 0, logger, add_sky=False) else: psf_im = None t6 = time.time() if logger: logger.debug('obj %d: Times: %f, %f, %f, %f, %f', obj_num, t2 - t1, t3 - t2, t4 - t3, t5 - t4, t6 - t5) except Exception as e: if itry == ntries - 1: # Then this was the last try. Just re-raise the exception. raise else: if logger: logger.info('Object %d: Caught exception %s', obj_num, str(e)) logger.info('This is try %d/%d, so trying again.', itry + 1, ntries) # Need to remove the "current_val"s from the config dict. Otherwise, # the value generators will do a quick return with the cached value. galsim.config.process.RemoveCurrent(config, keep_safe=True) continue
weight_im.setScale(im.scale) weight_im.setZero() else: weight_im = None if do_noise: if 'noise' in config['image']: AddNoisePhot(im,weight_im,config['image']['noise'],config,rng,sky_level_pixel, logger) elif sky_level_pixel: im += sky_level_pixel else: raise AttributeError("Unknown draw_method %s."%draw_method) if make_badpix_image: badpix_im = galsim.ImageS(im.bounds) badpix_im.setScale(im.scale) badpix_im.setZero() else: badpix_im = None t5 = time.time() if make_psf_image: psf_im = DrawPSFStamp(psf,pix,config,im.bounds,final_shift) else: psf_im = None t6 = time.time() if logger:
def test_celestial(): """Test using a (realistic) CelestialWCS for the main image. """ # Make a CelestialWCS. The simplest kind to make from scratch is a TanWCS. affine = galsim.AffineTransform(0.26, -0.02, 0.03, 0.28, world_origin=galsim.PositionD(912.4, -833.1)) ra = 13.2343 * galsim.hours dec = -39.8484 * galsim.degrees pointing = galsim.CelestialCoord(ra,dec) wcs = galsim.TanWCS(affine, world_origin=pointing) print('wcs = ',wcs) # Start with a larger image from which we will cut out the postage stamp full_image = galsim.Image(2048,2048, wcs=wcs) full_weight = galsim.ImageS(2048,2048, wcs=wcs, init_value=1) # Make a postage stamp cutout # This next bit is the same as we did for the EuclideanWCS size = 64 image_pos = galsim.PositionD(1083.9, 617.3) sky_pos = wcs.toWorld(image_pos) if galsim.__version__ >= '2.0': u,v = pointing.project(sky_pos) field_pos = galsim.PositionD(u/galsim.arcsec, v/galsim.arcsec) else: field_pos = pointing.project(sky_pos) icen = int(image_pos.x) jcen = int(image_pos.y) bounds = galsim.BoundsI(icen-size//2+1, icen+size//2, jcen-size//2+1, jcen+size//2) image = full_image[bounds] weight = full_weight[bounds] galsim.Gaussian(sigma=5).drawImage(image) weight += image # With a CelestialWCS, we need to supply a pointing stardata = piff.StarData(image, image_pos, weight=weight, pointing=pointing) # Test properties print('props = ',stardata.properties) np.testing.assert_equal(stardata['x'], image_pos.x) np.testing.assert_equal(stardata['y'], image_pos.y) np.testing.assert_equal(stardata['u'], field_pos.x) np.testing.assert_equal(stardata['v'], field_pos.y) np.testing.assert_equal(stardata['ra'], sky_pos.ra/galsim.hours) np.testing.assert_equal(stardata['dec'], sky_pos.dec/galsim.degrees) # Test access via getImage method: im, wt, pos = stardata.getImage() np.testing.assert_array_equal(im.array, image.array) np.testing.assert_array_equal(wt.array, weight.array) np.testing.assert_equal(pos, image_pos) # Test access via getDataVector method: for data, wt, u, v in np.array(stardata.getDataVector()).T: # u,v values should correspond to image coordinates via wcs uv = galsim.PositionD(u,v) + field_pos if galsim.__version__ >= '2.0': radec = pointing.deproject(uv.x * galsim.arcsec, uv.y * galsim.arcsec) else: radec = pointing.deproject(uv) xy = wcs.toImage(radec) # These should now be integers, but round in case of numerical inaccuracy. ix = int(round(xy.x)) jy = int(round(xy.y)) np.testing.assert_equal(data, image(ix,jy)) np.testing.assert_equal(wt, weight(ix,jy)) print("Passed tests of StarData with CelestialWCS")
def test_euclidean(): """Test a slightly more complicated WCS and an object not centered at the center of the image. """ # Make a non-trivial WCS wcs = galsim.AffineTransform(0.26, -0.02, 0.03, 0.28, world_origin=galsim.PositionD(912.4, -833.1)) print('wcs = ',wcs) # Start with a larger image from which we will cut out the postage stamp full_image = galsim.Image(2048,2048, wcs=wcs) full_weight = galsim.ImageS(2048,2048, wcs=wcs, init_value=1) print('origin of full image is at u,v = ',full_image.wcs.toWorld(full_image.origin)) print('center of full image is at u,v = ',full_image.wcs.toWorld(full_image.center)) # Make a postage stamp cutout size = 64 # This time, use an even size. image_pos = galsim.PositionD(1083.9, 617.3) field_pos = wcs.toWorld(image_pos) icen = int(image_pos.x) jcen = int(image_pos.y) bounds = galsim.BoundsI(icen-size//2+1, icen+size//2, jcen-size//2+1, jcen+size//2) image = full_image[bounds] weight = full_weight[bounds] print('image_pos = ',image_pos) print('field pos (u,v) = ',field_pos) print('origin of ps image is at u,v = ',image.wcs.toWorld(image.origin)) print('center of ps image is at u,v = ',image.wcs.toWorld(image.center)) # Just draw something so it has non-trivial pixel values. galsim.Gaussian(sigma=5).drawImage(image) weight += image stardata = piff.StarData(image, image_pos, weight=weight) # Test properties print('props = ',stardata.properties) np.testing.assert_equal(stardata['x'], image_pos.x) np.testing.assert_equal(stardata['y'], image_pos.y) np.testing.assert_equal(stardata['u'], field_pos.x) np.testing.assert_equal(stardata['v'], field_pos.y) # Shouldn't matter whether we use the original wcs or the one in the postage stamp. np.testing.assert_equal(stardata['u'], image.wcs.toWorld(image_pos).x) np.testing.assert_equal(stardata['v'], image.wcs.toWorld(image_pos).y) # Test access via getImage method: im, wt, pos = stardata.getImage() np.testing.assert_array_equal(im.array, image.array) np.testing.assert_array_equal(wt.array, weight.array) np.testing.assert_equal(pos, image_pos) # Test access via getDataVector method: for data, wt, u, v in np.array(stardata.getDataVector()).T: # u,v values should correspond to image coordinates via wcs uv = galsim.PositionD(u,v) + field_pos xy = wcs.toImage(uv) # These should now be integers, but round in case of numerical inaccuracy. ix = int(round(xy.x)) jy = int(round(xy.y)) np.testing.assert_equal(data, image(ix,jy)) np.testing.assert_equal(wt, weight(ix,jy)) print("Passed tests of StarData with EuclideanWCS")
def load_images(stars, file_name, pointing=None, image_hdu=None, weight_hdu=None, badpix_hdu=None, sky=None, logger=None): """Load the image data into a list of Stars. We don't store the image data for Stars when we write them to a file, since that would take up a lot of space and is usually not desired. However, we do store the bounds in the original image where the star was cutout, so if you want to load back in the original data from the image file, you can do so with this function. :param stars: A list of Star instances. :param file_name: The file with the image data for these stars. :param pointing: The pointing direction to use. [default: None] :param image_hdu: The hdu to use for the main image. [default: None, which means either 0 or 1 as appropriate according to the compression.] :param weight_hdu: The hdu to use for the weight image. [default: None] :param badpix_hdu: The hdu to use for the bad pixel mask. [default: None] :param sky: Optional sky image or float value to subtract from the main image. [default: None] :param logger: A logger object for logging debug info. [default: None] :returns: a new list of Stars with the images information loaded. """ import galsim # TODO: This is largely copied from InputHandler.readImages. # This should probably be refactored a bit to avoid the duplicated code. logger = galsim.config.LoggerWrapper(logger) logger.info("Loading image information from file %s", file_name) image = galsim.fits.read(file_name, hdu=image_hdu) if sky is not None: image = image - sky # Either read in the weight image, or build a dummy one if weight_hdu is None: logger.debug("Making trivial (wt==1) weight image") weight = galsim.ImageI(image.bounds, init_value=1) else: logger.info("Reading weight image from hdu %d.", weight_hdu) weight = galsim.fits.read(file_name, hdu=weight_hdu) if np.all(weight.array == 0): logger.error( "According to the weight mask in %s, all pixels have zero weight!", file_name) # If requested, set wt=0 for any bad pixels if badpix_hdu is not None: logger.info("Reading badpix image from hdu %d.", badpix_hdu) badpix = galsim.fits.read(file_name, hdu=badpix_hdu) # The badpix image may be offset by 32768 from the true value. # If so, subtract it off. if np.any(badpix.array > 32767): logger.debug('min(badpix) = %s', np.min(badpix.array)) logger.debug('max(badpix) = %s', np.max(badpix.array)) logger.debug( "subtracting 32768 from all values in badpix image") badpix -= 32768 if np.any(badpix.array < -32767): logger.debug('min(badpix) = %s', np.min(badpix.array)) logger.debug('max(badpix) = %s', np.max(badpix.array)) logger.debug("adding 32768 to all values in badpix image") badpix += 32768 # Also, convert to int16, in case it isn't by default. badpix = galsim.ImageS(badpix) if np.all(badpix.array != 0): logger.error( "According to the bad pixel array in %s, all pixels are masked!", file_name) weight.array[badpix.array != 0] = 0 stars = [ Star(data=StarData(image=image[star.data.image.bounds], image_pos=star.data.image_pos, weight=weight[star.data.image.bounds], pointing=(pointing if pointing is not None else star.data.pointing), properties=star.data.properties, _xyuv_set=True), fit=star.fit) for star in stars ] return stars
def BuildScatteredImage(config, logger=None, image_num=0, obj_num=0, make_psf_image=False, make_weight_image=False, make_badpix_image=False): """ Build an Image containing multiple objects placed at arbitrary locations. @param config A configuration dict. @param logger If given, a logger object to log progress. [default None] @param image_num If given, the current `image_num` [default: 0] @param obj_num If given, the current `obj_num` [default: 0] @param make_psf_image Whether to make `psf_image`. [default: False] @param make_weight_image Whether to make `weight_image`. [default: False] @param make_badpix_image Whether to make `badpix_image`. [default: False] @returns the tuple `(image, psf_image, weight_image, badpix_image)`. Note: All 4 Images are always returned in the return tuple, but the latter 3 might be None depending on the parameters make_*_image. """ config['index_key'] = 'image_num' config['image_num'] = image_num config['obj_num'] = obj_num if logger: logger.debug('image %d: BuildScatteredImage: image, obj = %d,%d', image_num,image_num,obj_num) if 'random_seed' in config['image'] and not isinstance(config['image']['random_seed'],dict): first = galsim.config.ParseValue(config['image'], 'random_seed', config, int)[0] config['image']['random_seed'] = { 'type' : 'Sequence', 'first' : first } nobjects = GetNObjForScatteredImage(config,image_num) if logger: logger.debug('image %d: nobj = %d',image_num,nobjects) ignore = [ 'random_seed', 'draw_method', 'noise', 'pixel_scale', 'wcs', 'nproc', 'sky_level', 'sky_level_pixel', 'retry_failures', 'image_pos', 'world_pos', 'n_photons', 'wmult', 'offset', 'stamp_size', 'stamp_xsize', 'stamp_ysize', 'gsparams', 'nobjects' ] opt = { 'size' : int , 'xsize' : int , 'ysize' : int , 'nproc' : int , 'index_convention' : str } params = galsim.config.GetAllParams( config['image'], 'image', config, opt=opt, ignore=ignore)[0] # Special check for the size. Either size or both xsize and ysize is required. if 'size' not in params: if 'xsize' not in params or 'ysize' not in params: raise AttributeError( "Either attribute size or both xsize and ysize required for image.type=Scattered") full_xsize = params['xsize'] full_ysize = params['ysize'] else: if 'xsize' in params: raise AttributeError( "Attributes xsize is invalid if size is set for image.type=Scattered") if 'ysize' in params: raise AttributeError( "Attributes ysize is invalid if size is set for image.type=Scattered") full_xsize = params['size'] full_ysize = params['size'] # If image_force_xsize and image_force_ysize were set in config, make sure it matches. if ( ('image_force_xsize' in config and full_xsize != config['image_force_xsize']) or ('image_force_ysize' in config and full_ysize != config['image_force_ysize']) ): raise ValueError( "Unable to reconcile required image xsize and ysize with provided "+ "xsize=%d, ysize=%d, "%(full_xsize,full_ysize)) config['image_xsize'] = full_xsize config['image_ysize'] = full_ysize convention = params.get('index_convention','1') _set_image_origin(config,convention) if logger: logger.debug('image %d: image_origin = %s',image_num,str(config['image_origin'])) logger.debug('image %d: image_center = %s',image_num,str(config['image_center'])) wcs = galsim.config.BuildWCS(config, logger) # Set the rng to use for image stuff. if 'random_seed' in config['image']: # Technically obj_num+nobjects will be the index of the random seed used for the next # image's first object (if there is a next image). But I don't think that will have # any adverse effects. config['obj_num'] = obj_num + nobjects config['index_key'] = 'obj_num' seed = galsim.config.ParseValue(config['image'], 'random_seed', config, int)[0] config['index_key'] = 'image_num' if logger: logger.debug('image %d: seed = %d',image_num,seed) rng = galsim.BaseDeviate(seed) else: rng = galsim.BaseDeviate() config['rng'] = rng if 'image_pos' in config['image'] and 'world_pos' in config['image']: raise AttributeError("Both image_pos and world_pos specified for Scattered image.") if 'image_pos' not in config['image'] and 'world_pos' not in config['image']: xmin = config['image_origin'].x xmax = xmin + full_xsize-1 ymin = config['image_origin'].y ymax = ymin + full_ysize-1 config['image']['image_pos'] = { 'type' : 'XY' , 'x' : { 'type' : 'Random' , 'min' : xmin , 'max' : xmax }, 'y' : { 'type' : 'Random' , 'min' : ymin , 'max' : ymax } } nproc = params.get('nproc',1) full_image = galsim.ImageF(full_xsize, full_ysize) full_image.setOrigin(config['image_origin']) full_image.wcs = wcs full_image.setZero() if make_psf_image: full_psf_image = galsim.ImageF(full_image.bounds, wcs=wcs) full_psf_image.setZero() else: full_psf_image = None if make_weight_image: full_weight_image = galsim.ImageF(full_image.bounds, wcs=wcs) full_weight_image.setZero() else: full_weight_image = None if make_badpix_image: full_badpix_image = galsim.ImageS(full_image.bounds, wcs=wcs) full_badpix_image.setZero() else: full_badpix_image = None # Sometimes an input field needs to do something special at the start of an image. if 'input' in config: for key in [ k for k in galsim.config.valid_input_types.keys() if k in config['input'] ]: if galsim.config.valid_input_types[key][4]: assert key in config fields = config['input'][key] if not isinstance(fields, list): fields = [ fields ] input_objs = config[key] for i in range(len(fields)): field = fields[i] input_obj = input_objs[i] func = eval(galsim.config.valid_input_types[key][4]) func(input_obj, field, config) stamp_images = galsim.config.BuildStamps( nobjects=nobjects, config=config, nproc=nproc, logger=logger,obj_num=obj_num, do_noise=False, make_psf_image=make_psf_image, make_weight_image=make_weight_image, make_badpix_image=make_badpix_image) images = stamp_images[0] psf_images = stamp_images[1] weight_images = stamp_images[2] badpix_images = stamp_images[3] current_vars = stamp_images[4] max_current_var = 0. for k in range(nobjects): # This is our signal that the object was skipped. if not images[k].bounds.isDefined(): continue bounds = images[k].bounds & full_image.bounds if False: logger.debug('image %d: full bounds = %s',image_num,str(full_image.bounds)) logger.debug('image %d: stamp %d bounds = %s',image_num,k,str(images[k].bounds)) logger.debug('image %d: Overlap = %s',image_num,str(bounds)) if bounds.isDefined(): full_image[bounds] += images[k][bounds] if make_psf_image: full_psf_image[bounds] += psf_images[k][bounds] if make_weight_image: full_weight_image[bounds] += weight_images[k][bounds] if make_badpix_image: full_badpix_image[bounds] |= badpix_images[k][bounds] else: if logger: logger.warn( "Object centered at (%d,%d) is entirely off the main image,\n"%( images[k].bounds.center().x, images[k].bounds.center().y) + "whose bounds are (%d,%d,%d,%d)."%( full_image.bounds.xmin, full_image.bounds.xmax, full_image.bounds.ymin, full_image.bounds.ymax)) if current_vars[k] > max_current_var: max_current_var = current_vars[k] # Mark that we are no longer doing a single galaxy by deleting image_pos from config top # level, so it cannot be used for things like wcs.pixelArea(image_pos). if 'image_pos' in config: del config['image_pos'] if 'noise' in config['image']: # Apply the noise to the full image draw_method = galsim.config.GetCurrentValue(config['image'],'draw_method') if max_current_var > 0: import numpy # Then there was whitening applied in the individual stamps. # But there could be a different variance in each postage stamp, so the first # thing we need to do is bring everything up to a common level. noise_image = galsim.ImageF(full_image.bounds) for k in range(nobjects): b = images[k].bounds & full_image.bounds if b.isDefined(): noise_image[b] += current_vars[k] # Update this, since overlapping postage stamps may have led to a larger # value in some pixels. max_current_var = numpy.max(noise_image.array) # Figure out how much noise we need to add to each pixel. noise_image = max_current_var - noise_image # Add it. full_image.addNoise(galsim.VariableGaussianNoise(rng,noise_image)) # Now max_current_var is how much noise is in each pixel. config['rng'] = rng galsim.config.AddNoise( config,draw_method,full_image,full_weight_image,max_current_var,logger) else: # If we aren't doing noise, we still may need to add a non-zero sky_level. # The same noise function does this with the 'skip' draw method. galsim.config.AddNoise( config,'skip',full_image,full_weight_image,max_current_var,logger) return full_image, full_psf_image, full_weight_image, full_badpix_image
def BuildTiledImage(config, logger=None, image_num=0, obj_num=0, make_psf_image=False, make_weight_image=False, make_badpix_image=False): """ Build an Image consisting of a tiled array of postage stamps. @param config A configuration dict. @param logger If given, a logger object to log progress. [default: None] @param image_num If given, the current `image_num`. [default: 0] @param obj_num If given, the current `obj_num`. [default: 0] @param make_psf_image Whether to make `psf_image`. [default: False] @param make_weight_image Whether to make `weight_image`. [default: False] @param make_badpix_image Whether to make `badpix_image`. [default: False] @returns the tuple `(image, psf_image, weight_image, badpix_image)`. Note: All 4 Images are always returned in the return tuple, but the latter 3 might be None depending on the parameters make_*_image. """ config['index_key'] = 'image_num' config['image_num'] = image_num config['obj_num'] = obj_num if logger: logger.debug('image %d: BuildTiledImage: image, obj = %d,%d',image_num,image_num,obj_num) if 'random_seed' in config['image'] and not isinstance(config['image']['random_seed'],dict): first = galsim.config.ParseValue(config['image'], 'random_seed', config, int)[0] config['image']['random_seed'] = { 'type' : 'Sequence', 'first' : first } ignore = [ 'random_seed', 'draw_method', 'noise', 'pixel_scale', 'wcs', 'nproc', 'sky_level', 'sky_level_pixel', 'retry_failures', 'image_pos', 'n_photons', 'wmult', 'offset', 'gsparams' ] req = { 'nx_tiles' : int , 'ny_tiles' : int } opt = { 'stamp_size' : int , 'stamp_xsize' : int , 'stamp_ysize' : int , 'border' : int , 'xborder' : int , 'yborder' : int , 'nproc' : int , 'index_convention' : str, 'order' : str } params = galsim.config.GetAllParams( config['image'], 'image', config, req=req, opt=opt, ignore=ignore)[0] nx_tiles = params['nx_tiles'] ny_tiles = params['ny_tiles'] nobjects = nx_tiles * ny_tiles config['nx_tiles'] = nx_tiles config['ny_tiles'] = ny_tiles if logger: logger.debug('image %d: n_tiles = %d, %d',image_num,nx_tiles,ny_tiles) stamp_size = params.get('stamp_size',0) stamp_xsize = params.get('stamp_xsize',stamp_size) stamp_ysize = params.get('stamp_ysize',stamp_size) config['tile_xsize'] = stamp_xsize config['tile_ysize'] = stamp_ysize if (stamp_xsize == 0) or (stamp_ysize == 0): raise AttributeError( "Both image.stamp_xsize and image.stamp_ysize need to be defined and != 0.") border = params.get("border",0) xborder = params.get("xborder",border) yborder = params.get("yborder",border) do_noise = xborder >= 0 and yborder >= 0 # TODO: Note: if one of these is < 0 and the other is > 0, then # this will add noise to the border region. Not exactly the # design, but I didn't bother to do the bookkeeping right to # make the borders pure 0 in that case. full_xsize = (stamp_xsize + xborder) * nx_tiles - xborder full_ysize = (stamp_ysize + yborder) * ny_tiles - yborder # If image_force_xsize and image_force_ysize were set in config, make sure it matches. if ( ('image_force_xsize' in config and full_xsize != config['image_force_xsize']) or ('image_force_ysize' in config and full_ysize != config['image_force_ysize']) ): raise ValueError( "Unable to reconcile required image xsize and ysize with provided "+ "nx_tiles=%d, ny_tiles=%d, "%(nx_tiles,ny_tiles) + "xborder=%d, yborder=%d\n"%(xborder,yborder) + "Calculated full_size = (%d,%d) "%(full_xsize,full_ysize)+ "!= required (%d,%d)."%(config['image_force_xsize'],config['image_force_ysize'])) config['image_xsize'] = full_xsize config['image_ysize'] = full_ysize if logger: logger.debug('image %d: image_size = %d, %d',image_num,full_xsize,full_ysize) convention = params.get('index_convention','1') _set_image_origin(config,convention) if logger: logger.debug('image %d: image_origin = %s',image_num,str(config['image_origin'])) logger.debug('image %d: image_center = %s',image_num,str(config['image_center'])) wcs = galsim.config.BuildWCS(config, logger) # Set the rng to use for image stuff. if 'random_seed' in config['image']: # Technically obj_num+nobjects will be the index of the random seed used for the next # image's first object (if there is a next image). But I don't think that will have # any adverse effects. config['obj_num'] = obj_num + nobjects config['index_key'] = 'obj_num' seed = galsim.config.ParseValue(config['image'], 'random_seed', config, int)[0] config['index_key'] = 'image_num' if logger: logger.debug('image %d: seed = %d',image_num,seed) rng = galsim.BaseDeviate(seed) else: rng = galsim.BaseDeviate() config['rng'] = rng # Make a list of ix,iy values according to the specified order: order = params.get('order','row').lower() if order.startswith('row'): ix_list = [ ix for iy in range(ny_tiles) for ix in range(nx_tiles) ] iy_list = [ iy for iy in range(ny_tiles) for ix in range(nx_tiles) ] elif order.startswith('col'): ix_list = [ ix for ix in range(nx_tiles) for iy in range(ny_tiles) ] iy_list = [ iy for ix in range(nx_tiles) for iy in range(ny_tiles) ] elif order.startswith('rand'): ix_list = [ ix for ix in range(nx_tiles) for iy in range(ny_tiles) ] iy_list = [ iy for ix in range(nx_tiles) for iy in range(ny_tiles) ] galsim.random.permute(rng, ix_list, iy_list) # Define a 'image_pos' field so the stamps can set their position appropriately in case # we need it for PowerSpectum or NFWHalo. x0 = (stamp_xsize-1)/2. + config['image_origin'].x y0 = (stamp_ysize-1)/2. + config['image_origin'].y dx = stamp_xsize + xborder dy = stamp_ysize + yborder config['image']['image_pos'] = { 'type' : 'XY' , 'x' : { 'type' : 'List', 'items' : [ x0 + ix*dx for ix in ix_list ] }, 'y' : { 'type' : 'List', 'items' : [ y0 + iy*dy for iy in iy_list ] } } nproc = params.get('nproc',1) full_image = galsim.ImageF(full_xsize, full_ysize) full_image.setOrigin(config['image_origin']) full_image.wcs = wcs full_image.setZero() if make_psf_image: full_psf_image = galsim.ImageF(full_image.bounds, wcs=wcs) full_psf_image.setZero() else: full_psf_image = None if make_weight_image: full_weight_image = galsim.ImageF(full_image.bounds, wcs=wcs) full_weight_image.setZero() else: full_weight_image = None if make_badpix_image: full_badpix_image = galsim.ImageS(full_image.bounds, wcs=wcs) full_badpix_image.setZero() else: full_badpix_image = None # Sometimes an input field needs to do something special at the start of an image. if 'input' in config: for key in [ k for k in galsim.config.valid_input_types.keys() if k in config['input'] ]: if galsim.config.valid_input_types[key][4]: assert key in config fields = config['input'][key] if not isinstance(fields, list): fields = [ fields ] input_objs = config[key] for i in range(len(fields)): field = fields[i] input_obj = input_objs[i] func = eval(galsim.config.valid_input_types[key][4]) func(input_obj, field, config) stamp_images = galsim.config.BuildStamps( nobjects=nobjects, config=config, nproc=nproc, logger=logger, obj_num=obj_num, xsize=stamp_xsize, ysize=stamp_ysize, do_noise=do_noise, make_psf_image=make_psf_image, make_weight_image=make_weight_image, make_badpix_image=make_badpix_image) images = stamp_images[0] psf_images = stamp_images[1] weight_images = stamp_images[2] badpix_images = stamp_images[3] current_vars = stamp_images[4] max_current_var = 0 for k in range(nobjects): # This is our signal that the object was skipped. if not images[k].bounds.isDefined(): continue if False: logger.debug('image %d: full bounds = %s',image_num,str(full_image.bounds)) logger.debug('image %d: stamp %d bounds = %s',image_num,k,str(images[k].bounds)) assert full_image.bounds.includes(images[k].bounds) b = images[k].bounds full_image[b] += images[k] if make_psf_image: full_psf_image[b] += psf_images[k] if make_weight_image: full_weight_image[b] += weight_images[k] if make_badpix_image: full_badpix_image[b] |= badpix_images[k] if current_vars[k] > max_current_var: max_current_var = current_vars[k] # Mark that we are no longer doing a single galaxy by deleting image_pos from config top # level, so it cannot be used for things like wcs.pixelArea(image_pos). if 'image_pos' in config: del config['image_pos'] # If didn't do noise above in the stamps, then need to do it here. if not do_noise: if 'noise' in config['image']: # If we didn't apply noise in each stamp, then we need to apply it now. draw_method = galsim.config.GetCurrentValue(config['image'],'draw_method') if max_current_var > 0: import numpy # Then there was whitening applied in the individual stamps. # But there could be a different variance in each postage stamp, so the first # thing we need to do is bring everything up to a common level. noise_image = galsim.ImageF(full_image.bounds) for k in range(nobjects): noise_image[images[k].bounds] += current_vars[k] # Update this, since overlapping postage stamps may have led to a larger # value in some pixels. max_current_var = numpy.max(noise_image.array) # Figure out how much noise we need to add to each pixel. noise_image = max_current_var - noise_image # Add it. full_image.addNoise(galsim.VariableGaussianNoise(rng,noise_image)) # Now max_current_var is how much noise is in each pixel. config['rng'] = rng galsim.config.AddNoise( config,draw_method,full_image,full_weight_image,max_current_var,logger) else: # If we aren't doing noise, we still may need to add a non-zero sky_level. # The same noise function does this with the 'skip' draw method. galsim.config.AddNoise( config,'skip',full_image,full_weight_image,max_current_var,logger) return full_image, full_psf_image, full_weight_image, full_badpix_image
def build_file(seed, file_name, mass, nobj, rng, truth_file_name, halo_id, first_obj_id): """A function that does all the work to build a single file. Returns the total time taken. """ t1 = time.time() # Build the image onto which we will draw the galaxies. full_image = galsim.ImageF(image_size, image_size) # The "true" center of the image is allowed to be halfway between two pixels, as is the # case for even-sized images. full_image.bounds.center() is an integer position, # which would be 1/2 pixel up and to the right of the true center in this case. im_center = full_image.bounds.trueCenter() # For the WCS, this time we use UVFunction, which lets you define arbitrary u(x,y) # and v(x,y) functions. We use a simple cubic radial function to create a # pincushion distortion. This is a typical kind of telescope distortion, although # we exaggerate the magnitude of the effect to make it more apparent. # The pixel size in the center of the image is 0.05, but near the corners (r=362), # the pixel size is approximately 0.075, which is much more distortion than is # normally present in typical telescopes. But it makes the effect of the variable # pixel area obvious when you look at the weight image in the output files. ufunc1 = lambda x, y: 0.05 * x * (1. + 2.e-6 * (x**2 + y**2)) vfunc1 = lambda x, y: 0.05 * y * (1. + 2.e-6 * (x**2 + y**2)) # It's not required to provide the inverse functions. However, if we don't, then # you will only be able to do toWorld operations, not the inverse toImage. # The inverse function does not have to be exact either. For example, you could provide # a function that does some kind of iterative solution to whatever accuracy you care # about. But in this case, we can do the exact inverse. # # Let w = sqrt(u**2 + v**2) and r = sqrt(x**2 + y**2). Then the solutions are: # x = (u/w) r and y = (u/w) r, and we use Cardano's method to solve for r given w: # See http://en.wikipedia.org/wiki/Cubic_function#Cardano.27s_method # # w = 0.05 r + 2.e-6 * 0.05 * r**3 # r = 100 * ( ( 5 sqrt(w**2 + 5.e3/27) + 5 w )**(1./3.) - # - ( 5 sqrt(w**2 + 5.e3/27) - 5 w )**(1./3.) ) def xfunc1(u, v): import math wsq = u * u + v * v if wsq == 0.: return 0. else: w = math.sqrt(wsq) temp = 5. * math.sqrt(wsq + 5.e3 / 27) r = 100. * ((temp + 5 * w)**(1. / 3.) - (temp - 5 * w)**(1. / 3)) return u * r / w def yfunc1(u, v): import math wsq = u * u + v * v if wsq == 0.: return 0. else: w = math.sqrt(wsq) temp = 5. * math.sqrt(wsq + 5.e3 / 27) r = 100. * ((temp + 5 * w)**(1. / 3.) - (temp - 5 * w)**(1. / 3)) return v * r / w # You could pass the above functions to UVFunction, and normally we would do that. # The only down side to doing so is that the specification of the WCS in the FITS # file is rather ugly. GalSim is able to turn the python byte code into strings, # but they are basically a really ugly mess of random-looking characters. GalSim # will be able to read it back in, but human readers will have no idea what WCS # function was used. To see what they look like, uncomment this line and comment # out the later wcs line. #wcs = galsim.UVFunction(ufunc1, vfunc1, xfunc1, yfunc1, origin=im_center) # If you provide the functions as strings, then those strings will be preserved # in the FITS header in a form that is more legible to human readers. # It also has the extra benefit of matching the output from demo9.yaml, which we # always try to do. The config file has no choice but to specify the functions # as strings. ufunc = '0.05 * x * (1. + 2.e-6 * (x**2 + y**2))' vfunc = '0.05 * y * (1. + 2.e-6 * (x**2 + y**2))' xfunc = ( '( lambda w: ( 0 if w==0 else ' + '100.*u/w*(( 5*(w**2 + 5.e3/27.)**0.5 + 5*w )**(1./3.) - ' + '( 5*(w**2 + 5.e3/27.)**0.5 - 5*w )**(1./3.))))( (u**2+v**2)**0.5 )' ) yfunc = ( '( lambda w: ( 0 if w==0 else ' + '100.*v/w*(( 5*(w**2 + 5.e3/27.)**0.5 + 5*w )**(1./3.) - ' + '( 5*(w**2 + 5.e3/27.)**0.5 - 5*w )**(1./3.))))( (u**2+v**2)**0.5 )' ) # The origin parameter defines where on the image should be considered (x,y) = (0,0) # in the WCS functions. wcs = galsim.UVFunction(ufunc, vfunc, xfunc, yfunc, origin=im_center) # Assign this wcs to full_image full_image.wcs = wcs # The weight image will hold the inverse variance for each pixel. # We can set the wcs directly on construction with the wcs parameter. weight_image = galsim.ImageF(image_size, image_size, wcs=wcs) # It is common for astrometric images to also have a bad pixel mask. We don't have any # defect simulation currently, so our bad pixel masks are currently all zeros. # But someday, we plan to add defect functionality to GalSim, at which point, we'll # be able to mark those defects on a bad pixel mask. # Note: the S in ImageS means to use "short int" for the data type. # This is a typical choice for a bad pixel image. badpix_image = galsim.ImageS(image_size, image_size, wcs=wcs) # We also draw a PSF image at the location of every galaxy. This isn't normally done, # and since some of the PSFs overlap, it's not necessarily so useful to have this kind # of image. But in this case, it's fun to look at the psf image, especially with # something like log scaling in ds9 to see how crazy an aberrated OpticalPSF with # struts can look when there is no atmospheric component to blur it out. psf_image = galsim.ImageF(image_size, image_size, wcs=wcs) # We will also write some truth information to an output catalog. # In real simulations, it is often useful to have a catalog of the truth values # to compare to measurements either directly or as cuts on the galaxy sample to # find where systematic errors are largest. # For now, we just make an empty OutputCatalog object with the names and types of the # columns. names = [ 'object_id', 'halo_id', 'flux', 'radius', 'h_over_r', 'inclination.rad', 'theta.rad', 'mu', 'redshift', 'shear.g1', 'shear.g2', 'pos.x', 'pos.y', 'image_pos.x', 'image_pos.y', 'halo_mass', 'halo_conc', 'halo_redshift' ] types = [ int, int, float, float, float, float, float, float, float, float, float, float, float, float, float, float, float, float ] truth_cat = galsim.OutputCatalog(names, types) # Setup the NFWHalo stuff: nfw = galsim.NFWHalo(mass=mass, conc=nfw_conc, redshift=nfw_z_halo, omega_m=omega_m, omega_lam=omega_lam) # Note: the last two are optional. If they are omitted, then (omega_m=0.3, omega_lam=0.7) # are actually the defaults. If you only specify one of them, the other is set so that # the total is 1. But you can define both values so that the total is not 1 if you want. # Radiation is assumed to be zero and dark energy equation of state w = -1. # If you want to include either radiation or more complicated dark energy models, # you can define your own cosmology class that defines the functions a(z), E(a), and # Da(z_source, z_lens). Then you can pass this to NFWHalo as a `cosmo` parameter. # Make the PSF profile outside the loop to minimize the (significant) OpticalPSF # construction overhead. psf = galsim.OpticalPSF(lam=psf_lam, diam=psf_D, obscuration=psf_obsc, nstruts=psf_nstruts, strut_thick=psf_strut_thick, strut_angle=psf_strut_angle, defocus=psf_defocus, astig1=psf_astig1, astig2=psf_astig2, coma1=psf_coma1, coma2=psf_coma2, trefoil1=psf_trefoil1, trefoil2=psf_trefoil2) for k in range(nobj): # Initialize the random number generator we will be using for this object: ud = galsim.UniformDeviate(seed + k + 1) # Determine where this object is going to go. # We choose points randomly within a donut centered at the center of the main image # in order to avoid placing galaxies too close to the halo center where the lensing # is not weak. We use an inner radius of 3 arcsec and an outer radius of 12 arcsec, # which takes us essentially to the edge of the image. radius = 12 inner_radius = 3 max_rsq = radius**2 min_rsq = inner_radius**2 while True: # (This is essentially a do..while loop.) x = (2. * ud() - 1) * radius y = (2. * ud() - 1) * radius rsq = x**2 + y**2 if rsq >= min_rsq and rsq <= max_rsq: break pos = galsim.PositionD(x, y) # We also need the position in pixels to determine where to place the postage # stamp on the full image. image_pos = wcs.toImage(pos) # For even-sized postage stamps, the nominal center (returned by stamp.bounds.center()) # cannot be at the true center (returned by stamp.bounds.trueCenter()) of the postage # stamp, since the nominal center values have to be integers. Thus, the nominal center # is 1/2 pixel up and to the right of the true center. # If we used odd-sized postage stamps, we wouldn't need to do this. x_nominal = image_pos.x + 0.5 y_nominal = image_pos.y + 0.5 # Get the integer values of these which will be the actual nominal center of the # postage stamp image. ix_nominal = int(math.floor(x_nominal + 0.5)) iy_nominal = int(math.floor(y_nominal + 0.5)) # The remainder will be accounted for in an offset when we draw. dx = x_nominal - ix_nominal dy = y_nominal - iy_nominal offset = galsim.PositionD(dx, dy) # Draw the flux from a power law distribution: N(f) ~ f^-1.5 # For this, we use the class DistDeviate which can draw deviates from an arbitrary # probability distribution. This distribution can be defined either as a functional # form as we do here, or as tabulated lists of x and p values, from which the # function is interpolated. flux_dist = galsim.DistDeviate(ud, function=lambda x: x**-1.5, x_min=gal_flux_min, x_max=gal_flux_max) flux = flux_dist() # We introduce here another surface brightness profile, called InclinedExponential. # It represents a typical 3D galaxy disk profile inclined at an arbitrary angle # relative to face on. # # inclination = 0 degrees corresponds to a face-on disk, which is equivalent to # the regular Exponential profile. # inclination = 90 degrees corresponds to an edge-on disk. # # A random orientation corresponds to the inclination angle taking the probability # distribution: # # P(inc) = 0.5 sin(inc) # # so we again use a DistDeviate to generate these values. inc_dist = galsim.DistDeviate(ud, function=lambda x: 0.5 * math.sin(x), x_min=0, x_max=math.pi) inclination = inc_dist() * galsim.radians # The parameters scale_radius and scale_height give the scale distances in the # 3D distribution: # # I(R,z) = I_0 / (2 scale_height) * sech^2(z/scale_height) * exp(-r/scale_radius) # # These values can be given separately if desired. However, it is often easier to # give the ratio scale_h_over_r as an independent value, since the radius and height # values are correlated, while h/r is approximately independent of h or r. h_over_r = ud() * (gal_h_over_r_max - gal_h_over_r_min) + gal_h_over_r_min radius = ud() * (gal_r_max - gal_r_min) + gal_r_min # The inclination is around the x-axis, so we want to rotate the galaxy by a # random angle. theta = ud() * math.pi * 2. * galsim.radians # Make the galaxy profile with these values: gal = galsim.InclinedExponential(scale_radius=radius, scale_h_over_r=h_over_r, inclination=inclination, flux=flux) gal = gal.rotate(theta) # Now apply the appropriate lensing effects for this position from # the NFW halo mass. try: g1, g2 = nfw.getShear(pos, nfw_z_source) nfw_shear = galsim.Shear(g1=g1, g2=g2) except: # This shouldn't happen, since we exclude the inner 10 arcsec, but it's a # good idea to use the try/except block here anyway. import warnings warnings.warn( "Warning: NFWHalo shear is invalid -- probably strong lensing! " + "Using shear = 0.") nfw_shear = galsim.Shear(g1=0, g2=0) nfw_mu = nfw.getMagnification(pos, nfw_z_source) if nfw_mu < 0: import warnings warnings.warn( "Warning: mu < 0 means strong lensing! Using mu=25.") nfw_mu = 25 elif nfw_mu > 25: import warnings warnings.warn( "Warning: mu > 25 means strong lensing! Using mu=25.") nfw_mu = 25 # Calculate the total shear to apply # Since shear addition is not commutative, it is worth pointing out that # the order is in the sense that the second shear is applied first, and then # the first shear. i.e. The field shear is taken to be behind the cluster. # Kind of a cosmic shear contribution between the source and the cluster. # However, this is not quite the same thing as doing: # gal.shear(field_shear).shear(nfw_shear) # since the shear addition ignores the rotation that would occur when doing the # above lines. This is normally ok, because the rotation is not observable, but # it is worth keeping in mind. total_shear = nfw_shear + field_shear # Apply the magnification and shear to the galaxy gal = gal.magnify(nfw_mu) gal = gal.shear(total_shear) # Build the final object final = galsim.Convolve([psf, gal]) # Draw the stamp image # To draw the image at a position other than the center of the image, you can # use the offset parameter, which applies an offset in pixels relative to the # center of the image. # We also need to provide the local wcs at the current position. local_wcs = wcs.local(image_pos) stamp = final.drawImage(wcs=local_wcs, offset=offset) # Recenter the stamp at the desired position: stamp.setCenter(ix_nominal, iy_nominal) # Find overlapping bounds bounds = stamp.bounds & full_image.bounds full_image[bounds] += stamp[bounds] # Also draw the PSF psf_stamp = galsim.ImageF( stamp.bounds) # Use same bounds as galaxy stamp psf.drawImage(psf_stamp, wcs=local_wcs, offset=offset) psf_image[bounds] += psf_stamp[bounds] # Add the truth information for this object to the truth catalog row = ((first_obj_id + k), halo_id, flux, radius, h_over_r, inclination.rad(), theta.rad(), nfw_mu, nfw_z_source, total_shear.g1, total_shear.g2, pos.x, pos.y, image_pos.x, image_pos.y, mass, nfw_conc, nfw_z_halo) truth_cat.addRow(row) # Add Poisson noise to the full image # Note: The normal calculation of Poission noise isn't quite correct right now. # The pixel area is variable, which means the amount of sky flux that enters each # pixel is also variable. The wcs classes have a function `makeSkyImage` which # will fill an image with the correct amount of sky flux given the sky level # in units of ADU/arcsec^2. We use the weight image as our work space for this. wcs.makeSkyImage(weight_image, sky_level) # Add this to the current full_image (temporarily). full_image += weight_image # Add Poisson noise, given the current full_image. # The config parser uses a different random number generator for file-level and # image-level values than for the individual objects. This makes it easier to # parallelize the calculation if desired. In fact, this is why we've been adding 1 # to each seed value all along. The seeds for the objects take the values # random_seed+1 .. random_seed+nobj. The seed for the image is just random_seed, # which we built already (below) when we calculated how many objects need to # be in each file. Use the same rng again here, since this is also at image scope. full_image.addNoise(galsim.PoissonNoise(rng)) # Subtract the sky back off. full_image -= weight_image # The weight image is nominally the inverse variance of the pixel noise. However, it is # common to exclude the Poisson noise from the objects themselves and only include the # noise from the sky photons. The variance of the noise is just the sky level, which is # what is currently in the weight_image. (If we wanted to include the variance from the # objects too, then we could use the full_image before we added the PoissonNoise to it.) # So all we need to do now is to invert the values in weight_image. weight_image.invertSelf() # Write the file to disk: galsim.fits.writeMulti( [full_image, badpix_image, weight_image, psf_image], file_name) # And write the truth catalog file truth_cat.write(truth_file_name) t2 = time.time() return t2 - t1
weight_im = galsim.ImageF(im.bounds, scale=im.scale) weight_im.setZero() else: weight_im = None if do_noise: if 'noise' in config['image']: AddNoisePhot(im, weight_im, config['image']['noise'], config, rng, sky_level_pixel, logger) elif sky_level_pixel: im += sky_level_pixel else: raise AttributeError("Unknown draw_method %s." % draw_method) if make_badpix_image: badpix_im = galsim.ImageS(im.bounds, scale=im.scale) badpix_im.setZero() else: badpix_im = None t5 = time.time() if make_psf_image: psf_im = DrawPSFStamp(psf, pix, config, im.bounds, final_shift) else: psf_im = None t6 = time.time() if logger: logger.debug(' Times: %f, %f, %f, %f, %f', t2 - t1, t3 - t2, t4 - t3,
def test_star(): # Star class is a pretty thin wrapper of StarData and StarFit classes. # Same setup as for test_euclidean wcs = galsim.AffineTransform(0.26, -0.02, 0.03, 0.28, world_origin=galsim.PositionD(912.4, -833.1)) size = 64 image_pos = galsim.PositionD(1083.9, 617.3) field_pos = wcs.toWorld(image_pos) icen = int(image_pos.x + 0.5) jcen = int(image_pos.y + 0.5) bounds = galsim.BoundsI(icen - size // 2 - 1, icen + size // 2, jcen - size // 2 - 1, jcen + size // 2) image = galsim.Image(bounds, wcs=wcs) weight = galsim.ImageS(bounds, wcs=wcs, init_value=1) galsim.Gaussian(sigma=5).drawImage(image) weight += image properties = { 'ra': 34.1234, 'dec': -15.567, 'color_ri': 0.5, 'color_iz': -0.2, 'chipnum': 3 } stardata = piff.StarData(image, image_pos, weight=weight, properties=properties) # Check access via star class star = piff.Star(stardata, None) # fit is optional, but must be explicitly None for key, value in stardata.properties.items(): np.testing.assert_equal(star[key], value) np.testing.assert_equal(star.image.array, image.array) np.testing.assert_equal(star.weight.array, weight.array) assert star.image_pos == image_pos print('field_pos = ', field_pos) print('star.field_pos = ', star.field_pos) assert star.field_pos == field_pos assert star.x == image_pos.x assert star.y == image_pos.y assert star.u == field_pos.x assert star.v == field_pos.y assert star.chipnum == 3 assert star.flux == 1. assert star.center == (0, 0) assert star.is_reserve == False star = star.withFlux(7) assert star.flux == 7. assert star.center == (0, 0) star = star.withFlux(17, center=(102, 22)) assert star.flux == 17. assert star.center == (102, 22) star = star.withFlux(center=(12, 20)) assert star.flux == 17. assert star.center == (12, 20) star = star.withFlux(flux=2) assert star.flux == 2. assert star.center == (12, 20) # Test using makeTarget star = piff.Star.makeTarget(properties=stardata.properties, image=image, weight=weight) np.testing.assert_equal(star.data['x'], image_pos.x) np.testing.assert_equal(star.data['y'], image_pos.y) np.testing.assert_equal(star.data['u'], field_pos.x) np.testing.assert_equal(star.data['v'], field_pos.y) # Shouldn't matter whether we use the original wcs or the one in the postage stamp. print('image.wcs = ', image.wcs) print('image_pos = ', image_pos) print('field_pos = ', field_pos) print('image.wcs.toWorld(image_pos) = ', image.wcs.toWorld(image_pos)) print('in star.data: ', star.data['u']) print('in star.data: ', star.data['v']) np.testing.assert_equal(star.data['u'], image.wcs.toWorld(image_pos).x) np.testing.assert_equal(star.data['v'], image.wcs.toWorld(image_pos).y) im, wt, pos = star.data.getImage() np.testing.assert_array_equal(im.array, image.array) np.testing.assert_array_equal(wt.array, weight.array) np.testing.assert_equal(pos, image_pos) # Some invalid parameter checks np.testing.assert_raises(TypeError, piff.Star.makeTarget, x=1, properties=stardata.properties, image=image, weight=weight) np.testing.assert_raises(TypeError, piff.Star.makeTarget, y=1, properties=stardata.properties, image=image, weight=weight) np.testing.assert_raises(TypeError, piff.Star.makeTarget, u=1, properties=stardata.properties, image=image, weight=weight) np.testing.assert_raises(TypeError, piff.Star.makeTarget, v=1, properties=stardata.properties, image=image, weight=weight) np.testing.assert_raises(TypeError, piff.Star.makeTarget, x=1, image=image, weight=weight) np.testing.assert_raises(TypeError, piff.Star.makeTarget, y=1, image=image, weight=weight) np.testing.assert_raises(TypeError, piff.Star.makeTarget, u=1, image=image, weight=weight) np.testing.assert_raises(TypeError, piff.Star.makeTarget, image=image, weight=weight) np.testing.assert_raises(TypeError, piff.Star.makeTarget, x=1, y=2, scale=4, wcs=image.wcs, image=image, weight=weight)