def draw(self, profiles, image, method, offset, config, base, logger): """ Draw the profiles onto the stamp. """ if 'stamp_xsize' not in base or 'stamp_ysize' not in base: raise RuntimeError( "stamp size must be given for stamp type=BlendSet") nx = base['stamp_xsize'] ny = base['stamp_ysize'] wcs = base['wcs'] if profiles is not None: # Then we haven't drawn the full image yet. # We need to draw an image large enough to contain each of the cutouts bounds = galsim.BoundsI(galsim.PositionI(0, 0)) for pos in self.neighbor_pos: image_pos = wcs.toImage(pos) # Convert to nearest integer position image_pos = galsim.PositionI(int(image_pos.x + 0.5), int(image_pos.y + 0.5)) bounds += image_pos bounds = bounds.withBorder(max(nx, ny) // 2 + 1) self.full_images = [] for prof in profiles: im = galsim.ImageF(bounds=bounds, wcs=wcs) galsim.config.DrawBasic(prof, im, method, offset - im.true_center, config, base, logger) self.full_images.append(im) # Figure out what bounds to use for the cutouts. k = base['obj_num'] - self.first if k == 0: center_pos = galsim.PositionI(0, 0) else: center_pos = self.neighbor_pos[k - 1] center_image_pos = wcs.toImage(center_pos) xmin = int(center_image_pos.x) - nx // 2 + 1 ymin = int(center_image_pos.y) - ny // 2 + 1 self.bounds = galsim.BoundsI(xmin, xmin + nx - 1, ymin, ymin + ny - 1) # Add up the cutouts from the profile images image.setZero() image.wcs = wcs for full_im in self.full_images: assert full_im.bounds.includes(self.bounds) image += full_im[self.bounds] # And also build the neighbor image for the deblend image self.neighbor_image = image.copy() self.neighbor_image -= self.full_images[k][self.bounds] # Save this in base for the deblend output base['blend_neighbor_image'] = self.neighbor_image return image
def test_copy(self): """ Test that copy() works """ pointing = CelestialCoord(64.82*galsim.degrees, -16.73*galsim.degrees) rotation = 116.8*galsim.degrees chip_name = 'R:1,2 S:2,2' wcs0 = LsstWCS(pointing, rotation, chip_name) wcs0 = wcs0._newOrigin(galsim.PositionI(112, 4)) wcs1 = wcs0.copy() self.assertEqual(wcs0, wcs1) wcs0 = wcs0._newOrigin(galsim.PositionI(66, 77)) self.assertNotEqual(wcs0, wcs1)
def SetupConfigImageSize(config, xsize, ysize, logger=None): """Do some further setup of the config dict at the image processing level based on the provided image size. - Set config['image_xsize'], config['image_ysize'] to the size of the image - Set config['image_origin'] to the origin of the image - Set config['image_center'] to the center of the image - Set config['image_bounds'] to the bounds of the image - Build the WCS based on either config['image']['wcs'] or config['image']['pixel_scale'] - Set config['wcs'] to be the built wcs - If wcs.isPixelScale(), also set config['pixel_scale'] for convenience. - Set config['world_center'] to either a given value or based on wcs and image_center Parameters: config: The configuration dict. xsize: The size of the image in the x-dimension. ysize: The size of the image in the y-dimension. logger: If given, a logger object to log progress. [default: None] """ logger = galsim.config.LoggerWrapper(logger) config['image_xsize'] = xsize config['image_ysize'] = ysize image = config['image'] origin = 1 # default if 'index_convention' in image: convention = galsim.config.ParseValue(image, 'index_convention', config, str)[0] if convention.lower() in ('0', 'c', 'python'): origin = 0 elif convention.lower() in ('1', 'fortran', 'fits'): origin = 1 else: raise galsim.GalSimConfigValueError( "Unknown index_convention", convention, ('0', 'c', 'python', '1', 'fortran', 'fits')) config['image_origin'] = galsim.PositionI(origin, origin) config['image_center'] = galsim.PositionD(origin + (xsize - 1.) / 2., origin + (ysize - 1.) / 2.) config['image_bounds'] = galsim.BoundsI(origin, origin + xsize - 1, origin, origin + ysize - 1) # Build the wcs wcs = galsim.config.BuildWCS(image, 'wcs', config, logger) config['wcs'] = wcs # If the WCS is a PixelScale or OffsetWCS, then store the pixel_scale in base. The # config apparatus does not use it -- we always use the wcs -- but we keep it in case # the user wants to use it for an Eval item. It's one of the variables they are allowed # to assume will be present for them. if wcs.isPixelScale(): config['pixel_scale'] = wcs.scale # Set world_center if 'world_center' in image: config['world_center'] = galsim.config.ParseValue( image, 'world_center', config, galsim.CelestialCoord)[0] else: config['world_center'] = wcs.toWorld(config['image_center'])
def test_eq(self): """ Test that __eq__ works for LsstWCS """ start = time.clock() wcs1 = LsstWCS(self.pointing, self.rotation, self.chip_name) self.assertEqual(self.wcs, wcs1) new_origin = galsim.PositionI(9, 9) wcs1 = wcs1._newOrigin(new_origin) self.assertNotEqual(self.wcs, wcs1) other_pointing = CelestialCoord(1.9*galsim.degrees, -34.0*galsim.degrees) wcs2 = LsstWCS(other_pointing, self.rotation, self.chip_name) self.assertNotEqual(self.wcs, wcs2) wcs3 = LsstWCS(self.pointing, 112.0*galsim.degrees, self.chip_name) self.assertNotEqual(self.wcs, wcs3) wcs4 = LsstWCS(self.pointing, self.rotation, 'R:2,2 S:2,2') self.assertNotEqual(self.wcs, wcs4) print 'time to run %s = %e sec' % (funcname(), time.clock()-start)
def construct_image(self, band_index, uniform_deviate): world_origin = self.image_parameters.world_origin.as_galsim_position() degrees_per_pixel = self.image_parameters.degrees_per_pixel() wcs = ( # Here we implement the confusing mapping X <-> Dec, Y <-> RA galsim.JacobianWCS(0, degrees_per_pixel, degrees_per_pixel, 0).withOrigin(galsim.PositionI(0, 0), world_origin=world_origin)) image = galsim.ImageF( self.image_parameters.width_px, self.image_parameters.height_px, wcs=wcs, ) for index, light_source in enumerate(self._light_sources): sys.stdout.write('Band {} source {}\r'.format( band_index + 1, index + 1)) sys.stdout.flush() galsim_light_source = light_source.get_galsim_light_source( band_index, self.psf_sigma_pixels * self.image_parameters.degrees_per_pixel(), self.image_parameters, ) galsim_light_source.drawImage( image, add_to_image=True, method='phot', max_extra_noise=self.band_sky_level_nmgy[band_index] * self.image_parameters.band_nelec_per_nmgy[band_index] / 1000.0, rng=uniform_deviate, ) self._add_sky_background(image, band_index, uniform_deviate) return image
def test_position_type_promotion(): pd1 = galsim.PositionD(0.1, 0.2) pd2 = galsim.PositionD(-0.3, 0.4) pd3 = galsim.PositionD() # Also test 0-argument initializer here pi1 = galsim.PositionI(3, 65) pi2 = galsim.PositionI(-4, 4) pi3 = galsim.PositionI() # First check combinations that should yield a PositionD for lhs, rhs in zip([pd1, pd1, pi1, pd1, pi2], [pd2, pi1, pd2, pi3, pd3]): assert lhs + rhs == galsim.PositionD(lhs.x + rhs.x, lhs.y + rhs.y) assert lhs - rhs == galsim.PositionD(lhs.x - rhs.x, lhs.y - rhs.y) # Also check PosI +/- PosI -> PosI assert pi1 + pi2 == galsim.PositionI(pi1.x + pi2.x, pi1.y + pi2.y) assert pi1 - pi2 == galsim.PositionI(pi1.x - pi2.x, pi1.y - pi2.y)
def updateSkip(self, prof, image, method, offset, config, base, logger): """Before drawing the profile, see whether this object can be trivially skipped. The base method checks if the object is completely off the main image, so the intersection bounds will be undefined. In this case, don't bother drawing the postage stamp for this object. Parameters: prof: The profile to draw. image: The image onto which to draw the profile (which may be None). method: The method to use in drawImage. offset: The offset to apply when drawing. config: The configuration dict for the stamp field. base: The base configuration dict. logger: If given, a logger object to log progress. Returns: whether to skip drawing this object. """ if isinstance(prof, galsim.GSObject) and base.get( 'current_image', None) is not None: if image is None: prof = base['wcs'].toImage(prof, image_pos=base['image_pos']) N = prof.getGoodImageSize(1.) N += 2 + int(np.abs(offset.x) + np.abs(offset.y)) bounds = galsim._BoundsI(1, N, 1, N) else: bounds = image.bounds # Set the origin appropriately stamp_center = base['stamp_center'] if stamp_center: bounds = bounds.shift(stamp_center - bounds.center) else: bounds = bounds.shift( base.get('image_origin', galsim.PositionI(1, 1)) - galsim.PositionI(bounds.xmin, bounds.ymin)) overlap = bounds & base['current_image'].bounds if not overlap.isDefined(): logger.info( 'obj %d: skip drawing object because its image will be entirely off ' 'the main image.', base['obj_num']) return True return False
def _set_image_origin(config, convention): """Set config['image_origin'] appropriately based on the provided convention. """ if convention.lower() in [ '0', 'c', 'python' ]: origin = 0 elif convention.lower() in [ '1', 'fortran', 'fits' ]: origin = 1 else: raise AttributeError("Unknown index_convention: %s"%convention) config['image_origin'] = galsim.PositionI(origin,origin)
def test_copy(self): """ Test that copy() works """ start = time.clock() pointing = CelestialCoord(64.82*galsim.degrees, -16.73*galsim.degrees) rotation = 116.8*galsim.degrees chip_name = 'R:1,2 S:2,2' wcs0 = LsstWCS(pointing, rotation, chip_name) wcs0 = wcs0._newOrigin(galsim.PositionI(112, 4)) wcs1 = wcs0.copy() self.assertEqual(wcs0, wcs1) wcs0 = wcs0._newOrigin(galsim.PositionI(66, 77)) self.assertNotEqual(wcs0, wcs1) print 'time to run %s = %e sec' % (funcname(), time.clock()-start)
def __init__(self, i_gal, stamp_size, gal_model, st_model, pointing, sca_center, real_wcs=False): self.i_gal = i_gal self.stamp_size = stamp_size self.gal_model = gal_model self.st_model = st_model self.pointing = pointing self.sca_center = sca_center self.real_wcs = real_wcs self.stamp_size_factor = old_div( int(self.gal_model.getGoodImageSize(wfirst.pixel_scale)), self.stamp_size) if self.stamp_size_factor == 0: self.stamp_size_factor = 1 self.wcs, self.sky_level = self.pointing.get_wcs() self.xy = self.wcs.toImage(self.sca_center) # galaxy position if self.real_wcs == True: self.xyI = galsim.PositionI(int(self.xy.x), int(self.xy.y)) self.b = galsim.BoundsI( xmin=self.xyI.x - old_div(int(self.stamp_size_factor * self.stamp_size), 2) + 1, ymin=self.xyI.y - old_div(int(self.stamp_size_factor * self.stamp_size), 2) + 1, xmax=self.xyI.x + old_div(int(self.stamp_size_factor * self.stamp_size), 2), ymax=self.xyI.y + old_div(int(self.stamp_size_factor * self.stamp_size), 2)) else: self.xyI = galsim.PositionI( int(self.stamp_size_factor * self.stamp_size), int(self.stamp_size_factor * self.stamp_size)) self.b = galsim.BoundsI(xmin=1, xmax=self.xyI.x, ymin=1, ymax=self.xyI.y)
def draw_image(self, gal_model, st_model): self.make_stamp() if self.real_wcs == True: offset = self.xy - self.gal_stamp.true_center # original galaxy position - stamp center else: offset = galsim.PositionI(0, 0) gal_model.drawImage(image=self.gal_stamp, offset=offset) st_model.drawImage(image=self.psf_stamp, offset=offset) return self.gal_stamp, self.psf_stamp, offset
def accumulate(self, photons, image, orig_center=galsim.PositionI(0, 0)): """Accumulate the photons incident at the surface of the sensor into the appropriate pixels in the image. @param photons A PhotonArray instance describing the incident photons @param image The image into which the photons should be accumuated. @param orig_center The position of the image center in the original image coordinates. [default: (0,0)] """ return self._silicon.accumulate(photons, self.rng, image._image.view(), orig_center)
def test_galsim_bounds_error(): """Test basic usage of GalSimBoundsError """ pos = galsim.PositionI(0,0) bounds = galsim.BoundsI(1,10,1,10) err = galsim.GalSimBoundsError("Test", pos, bounds) print('str = ',str(err)) print('repr = ',repr(err)) assert str(err) == "Test galsim.PositionI(0,0) not in galsim.BoundsI(1,10,1,10)" assert err.pos == pos assert err.bounds == bounds assert isinstance(err, galsim.GalSimError) assert isinstance(err, ValueError) do_pickle(err)
def _set_image_origin(config, convention): """Set `config['image_origin']` appropriately based on the provided `convention`. """ if convention.lower() in [ '0', 'c', 'python' ]: origin = 0 elif convention.lower() in [ '1', 'fortran', 'fits' ]: origin = 1 else: raise AttributeError("Unknown index_convention: %s"%convention) config['image_origin'] = galsim.PositionI(origin,origin) # Also define the overall image center while we're at it. xsize = config['image_xsize'] ysize = config['image_ysize'] config['image_center'] = galsim.PositionD( origin + (xsize-1.)/2., origin + (ysize-1.)/2. )
def SetupConfigImageSize(config, xsize, ysize): """Do some further setup of the config dict at the image processing level based on the provided image size. - Set config['image_xsize'], config['image_ysize'] to the size of the image - Set config['image_origin'] to the origin of the image - Set config['image_center'] to the center of the image - Set config['image_bounds'] to the bounds of the image - Build the WCS based on either config['image']['wcs'] or config['image']['pixel_scale'] - Set config['wcs'] to be the built wcs - If wcs.isPixelScale(), also set config['pixel_scale'] for convenience. @param config The configuration dict. @param xsize The size of the image in the x-dimension. @param ysize The size of the image in the y-dimension. """ config['image_xsize'] = xsize config['image_ysize'] = ysize origin = 1 # default if 'index_convention' in config['image']: convention = galsim.config.ParseValue(config['image'], 'index_convention', config, str)[0] if convention.lower() in ['0', 'c', 'python']: origin = 0 elif convention.lower() in ['1', 'fortran', 'fits']: origin = 1 else: raise AttributeError("Unknown index_convention: %s" % convention) config['image_origin'] = galsim.PositionI(origin, origin) config['image_center'] = galsim.PositionD(origin + (xsize - 1.) / 2., origin + (ysize - 1.) / 2.) config['image_bounds'] = galsim.BoundsI(origin, origin + xsize - 1, origin, origin + ysize - 1) # Build the wcs wcs = galsim.config.BuildWCS(config) config['wcs'] = wcs # If the WCS is a PixelScale or OffsetWCS, then store the pixel_scale in base. The # config apparatus does not use it -- we always use the wcs -- but we keep it in case # the user wants to use it for an Eval item. It's one of the variables they are allowed # to assume will be present for them. if wcs.isPixelScale(): config['pixel_scale'] = wcs.scale
def test_round_trip(self): """ Test writing out an image with an LsstWCS, reading it back in, and comparing the resulting pixel -> ra, dec mappings """ start = time.clock() path, filename = os.path.split(__file__) im0 = galsim.Image(int(4000), int(4000), wcs=self.wcs) outputFile = os.path.join(path,'scratch_space','lsst_roundtrip_img.fits') if os.path.exists(outputFile): os.unlink(outputFile) im0.write(outputFile) im1 = galsim.fits.read(outputFile) xPix = [] yPix = [] pixPts = [] for xx in range(0, 4000, 100): for yy in range(0, 4000, 100): xPix.append(xx) yPix.append(yy) pixPts.append(galsim.PositionI(xx, yy)) xPix = np.array(xPix) yPix = np.array(yPix) ra_control, dec_control = self.wcs._radec(xPix, yPix) for rr, dd, pp in zip(ra_control, dec_control, pixPts): ra_dec_test = im1.wcs.toWorld(pp) self.assertAlmostEqual(rr, ra_dec_test.ra/galsim.radians, 12) self.assertAlmostEqual(dd, ra_dec_test.dec/galsim.radians, 12) if os.path.exists(outputFile): os.unlink(outputFile) print 'time to run %s = %e sec' % (funcname(), time.clock()-start)
def get_wcs(self): #self.find_coordinates() WCS = wfirst.getWCS(world_pos = galsim.CelestialCoord(ra=self.ra*galsim.radians, \ dec=self.dec*galsim.radians), PA = self.position_angle*galsim.radians, date = self.date, SCAs = self.sca, PA_is_FPA = True )[self.sca] sky_level = wfirst.getSkyLevel(self.bpass, world_pos=WCS.toWorld( galsim.PositionI( old_div(wfirst.n_pix, 2), old_div(wfirst.n_pix, 2))), date=self.date) sky_level *= (1.0 + wfirst.stray_light_fraction) * ( wfirst.pixel_scale )**2 # adds stray light and converts to photons/cm^2 sky_level *= self.stamp_size * self.stamp_size return WCS, sky_level
def cutout_psfs(self, psf, wcs): """ Grab square PSF cutout images :param psf: a DES_PSFEx instance :param wcs: (astropy.WCS) the wcs for the tile :return: 3D Numpy array """ object_x, object_y = self.get_object_xy(wcs) psf_cutouts = np.empty( (len(self.coadd_ids), self.psf_cutout_size, self.psf_cutout_size), dtype=np.double) for i, (x, y) in enumerate(zip(object_x, object_y)): pos = galsim.PositionI(x, y) psfimg = psf.getPSFArray(pos) center = (psfimg.shape[0] // 2, psfimg.shape[1] // 2) psfimg = self.single_cutout(psfimg, center, self.psf_cutout_size) psf_cutouts[i] = psfimg return psf_cutouts
def addnoise(self, stamp, ivarstamp): """Add noise to the object postage stamp. Remember that STAMP and IVARSTAMP are in units of nanomaggies and 1/nanomaggies**2, respectively. """ varstamp = ivarstamp.copy() varstamp.invertSelf() if np.min(varstamp.array) < 0: print(np.min(varstamp.array)) #sys.exit(1) # Add the variance of the object to the variance image (in electrons). stamp *= self.nano2e # [electron] #stamp.array = np.abs(stamp.array) st = np.abs(stamp.array) stamp = galsim.Image(st) varstamp *= self.nano2e**2 # [electron^2] firstvarstamp = varstamp + stamp # Add Poisson noise stamp.addNoise( galsim.VariableGaussianNoise(galsim.BaseDeviate(), firstvarstamp)) # ensure the Poisson variance from the object is >0 (see Galsim.demo13) objvar = galsim.Image(np.sqrt(stamp.array**2), scale=stamp.scale) objvar.setOrigin(galsim.PositionI(stamp.xmin, stamp.ymin)) varstamp += objvar # Convert back to [nanomaggies] stamp /= self.nano2e varstamp /= self.nano2e**2 ivarstamp = varstamp.copy() ivarstamp.invertSelf() return stamp, ivarstamp
def get_wcs(dither_i, sca, filter_, stamp_size, random_angle): dither_i = dither_i sca = sca filter_ = filter_ bpass = wfirst.getBandpasses(AB_zeropoint=True)[filter_] d = fio.FITS('observing_sequence_hlsonly_5yr.fits')[-1][dither_i] ra = d['ra'] * np.pi / 180. # RA of pointing dec = d['dec'] * np.pi / 180. # Dec of pointing #pa = d['pa'] * np.pi / 180. # Position angle of pointing date = Time(d['date'], format='mjd').datetime #random_dir = galsim.UniformDeviate(314) #pa = math.pi * random_dir() pa = random_angle * np.pi / 180. WCS = wfirst.getWCS(world_pos = galsim.CelestialCoord(ra=ra*galsim.radians, \ dec=dec*galsim.radians), PA = pa*galsim.radians, date = date, SCAs = sca, PA_is_FPA = True )[sca] sky_level = wfirst.getSkyLevel(bpass, world_pos=WCS.toWorld( galsim.PositionI( old_div(wfirst.n_pix, 2), old_div(wfirst.n_pix, 2))), date=date) sky_level *= (1.0 + wfirst.stray_light_fraction) * ( wfirst.pixel_scale)**2 # adds stray light and converts to photons/cm^2 sky_level *= stamp_size * stamp_size return WCS, sky_level
def test_draw(): """Test the various options of the PSF.draw command. """ if __name__ == '__main__': logger = piff.config.setup_logger(verbose=2) else: logger = piff.config.setup_logger(log_file='output/test_draw.log') # Use an existing Piff solution to match as closely as possible how users would actually # use this function. psf = piff.read('input/test_single_py27.piff', logger=logger) # Data that was used to make that file. wcs = galsim.TanWCS( galsim.AffineTransform(0.26, 0.05, -0.08, -0.24, galsim.PositionD(1024, 1024)), galsim.CelestialCoord(-5 * galsim.arcmin, -25 * galsim.degrees)) data = fitsio.read('input/test_single_cat1.fits') field_center = galsim.CelestialCoord(0 * galsim.degrees, -25 * galsim.degrees) chipnum = 1 for k in range(len(data)): x = data['x'][k] y = data['y'][k] e1 = data['e1'][k] e2 = data['e2'][k] s = data['s'][k] print('k,x,y = ', k, x, y) #print(' true s,e1,e2 = ',s,e1,e2) # First, the same test with this file that is in test_wcs.py:test_pickle() image_pos = galsim.PositionD(x, y) star = piff.Star.makeTarget(x=x, y=y, wcs=wcs, stamp_size=48, pointing=field_center, chipnum=chipnum) star = psf.drawStar(star) #print(' fitted s,e1,e2 = ',star.fit.params) np.testing.assert_almost_equal(star.fit.params, [s, e1, e2], decimal=6) # Now use the regular PSF.draw() command. This version is equivalent to the above. # (It's not equal all the way to machine precision, but pretty close.) im1 = psf.draw(x, y, chipnum, stamp_size=48) np.testing.assert_allclose(im1.array, star.data.image.array, rtol=1.e-14, atol=1.e-14) # The wcs in the image is the wcs of the original image assert im1.wcs == psf.wcs[1] # The image is 48 x 48 assert im1.array.shape == (48, 48) # The bounds are centered close to x,y. Within 0.5 pixel. np.testing.assert_allclose(im1.bounds.true_center.x, x, atol=0.5) np.testing.assert_allclose(im1.bounds.true_center.y, y, atol=0.5) # This version draws the star centered at (x,y). Check the hsm centroid. hsm = im1.FindAdaptiveMom() #print('hsm = ',hsm) np.testing.assert_allclose(hsm.moments_centroid.x, x, atol=0.01) np.testing.assert_allclose(hsm.moments_centroid.y, y, atol=0.01) # The total flux should be close to 1. np.testing.assert_allclose(im1.array.sum(), 1.0, rtol=1.e-3) # We can center the star at an arbitrary location on the image. # The default is equivalent to center=(x,y). So check that this is equivalent. # Also, 48 is the default stamp size, so that can be omitted here. im2 = psf.draw(x, y, chipnum, center=(x, y)) assert im2.bounds == im1.bounds np.testing.assert_allclose(im2.array, im1.array, rtol=1.e-14, atol=1.e-14) # Moving by an integer number of pixels should be very close to the same image # over a different slice of the array. im3 = psf.draw(x, y, chipnum, center=(x + 1, y + 3)) assert im3.bounds == im1.bounds # (Remember -- numpy indexing is y,x!) # Also, the FFTs will be different in detail, so only match to 1.e-6. #print('im1 argmax = ',np.unravel_index(np.argmax(im1.array),im1.array.shape)) #print('im3 argmax = ',np.unravel_index(np.argmax(im3.array),im3.array.shape)) np.testing.assert_allclose(im3.array[3:, 1:], im1.array[:-3, :-1], rtol=1.e-6, atol=1.e-6) hsm = im3.FindAdaptiveMom() np.testing.assert_allclose(hsm.moments_centroid.x, x + 1, atol=0.01) np.testing.assert_allclose(hsm.moments_centroid.y, y + 3, atol=0.01) # Can center at other locations, and the hsm centroids should come out centered pretty # close to that location. # (Of course the array will be different here, so can't test that.) im4 = psf.draw(x, y, chipnum, center=(x + 1.3, y - 0.8)) assert im4.bounds == im1.bounds hsm = im4.FindAdaptiveMom() np.testing.assert_allclose(hsm.moments_centroid.x, x + 1.3, atol=0.01) np.testing.assert_allclose(hsm.moments_centroid.y, y - 0.8, atol=0.01) # Also allowed is center=True to place in the center of the image. im5 = psf.draw(x, y, chipnum, center=True) assert im5.bounds == im1.bounds assert im5.array.shape == (48, 48) np.testing.assert_allclose(im5.bounds.true_center.x, x, atol=0.5) np.testing.assert_allclose(im5.bounds.true_center.y, y, atol=0.5) np.testing.assert_allclose(im5.array.sum(), 1., rtol=1.e-3) hsm = im5.FindAdaptiveMom() center = im5.true_center np.testing.assert_allclose(hsm.moments_centroid.x, center.x, atol=0.01) np.testing.assert_allclose(hsm.moments_centroid.y, center.y, atol=0.01) # Some invalid ways to try to do this. (Must be either True or a tuple.) np.testing.assert_raises(ValueError, psf.draw, x, y, chipnum, center='image') np.testing.assert_raises(ValueError, psf.draw, x, y, chipnum, center=im5.true_center) # If providing your own image with bounds far away from the star (say centered at 0), # then center=True works fine to draw in the center of that image. im6 = im5.copy() im6.setCenter(0, 0) psf.draw(x, y, chipnum, center=True, image=im6) assert im6.bounds.center == galsim.PositionI(0, 0) np.testing.assert_allclose(im6.array.sum(), 1., rtol=1.e-3) hsm = im6.FindAdaptiveMom() center = im6.true_center np.testing.assert_allclose(hsm.moments_centroid.x, center.x, atol=0.01) np.testing.assert_allclose(hsm.moments_centroid.y, center.y, atol=0.01) np.testing.assert_allclose(im6.array, im5.array, rtol=1.e-14, atol=1.e-14) # Check non-even stamp size. Also, not unit flux while we're at it. im7 = psf.draw(x, y, chipnum, center=(x + 1.3, y - 0.8), stamp_size=43, flux=23.7) assert im7.array.shape == (43, 43) np.testing.assert_allclose(im7.bounds.true_center.x, x, atol=0.5) np.testing.assert_allclose(im7.bounds.true_center.y, y, atol=0.5) np.testing.assert_allclose(im7.array.sum(), 23.7, rtol=1.e-3) hsm = im7.FindAdaptiveMom() np.testing.assert_allclose(hsm.moments_centroid.x, x + 1.3, atol=0.01) np.testing.assert_allclose(hsm.moments_centroid.y, y - 0.8, atol=0.01) # Can't do mixed even/odd shape with stamp_size, but it will respect a provided image. im8 = galsim.Image(43, 44) im8.setCenter( x, y ) # It will respect the given bounds, so put it near the right place. psf.draw(x, y, chipnum, center=(x + 1.3, y - 0.8), image=im8, flux=23.7) assert im8.array.shape == (44, 43) np.testing.assert_allclose(im8.array.sum(), 23.7, rtol=1.e-3) hsm = im8.FindAdaptiveMom() np.testing.assert_allclose(hsm.moments_centroid.x, x + 1.3, atol=0.01) np.testing.assert_allclose(hsm.moments_centroid.y, y - 0.8, atol=0.01) # The offset parameter can add an additional to whatever center is used. # Here center=None, so this is equivalent to im4 above. im9 = psf.draw(x, y, chipnum, offset=(1.3, -0.8)) assert im9.bounds == im1.bounds hsm = im9.FindAdaptiveMom() np.testing.assert_allclose(im9.array, im4.array, rtol=1.e-14, atol=1.e-14) # With both, they are effectively added together. Not sure if there would be a likely # use for this, but it's allowed. (The above with default center is used in unit # tests a number of times, so that version at least is useful if only for us. # I'm hard pressed to imaging end users wanting to specify things this way though.) im10 = psf.draw(x, y, chipnum, center=(x + 0.8, y - 0.3), offset=(0.5, -0.5)) assert im10.bounds == im1.bounds np.testing.assert_allclose(im10.array, im4.array, rtol=1.e-14, atol=1.e-14)
def test_output_catalog(): """Test basic operations on Catalog.""" import time t1 = time.time() names = [ 'float1', 'float2', 'int1', 'int2', 'bool1', 'bool2', 'str1', 'str2', 'str3', 'str4', 'angle', 'posi', 'posd', 'shear' ] types = [ float, float, int, int, bool, bool, str, str, str, str, galsim.Angle, galsim.PositionI, galsim.PositionD, galsim.Shear ] out_cat = galsim.OutputCatalog(names, types) out_cat.addRow([ 1.234, 4.131, 9, -3, 1, True, "He's", '"ceased', 'to', 'be"', 1.2 * galsim.degrees, galsim.PositionI(5, 6), galsim.PositionD(0.3, -0.4), galsim.Shear(g1=0.2, g2=0.1) ]) out_cat.addRow((2.345, -900, 0.0, 8, False, 0, "bleedin'", '"bereft', 'of', 'life"', 11 * galsim.arcsec, galsim.PositionI(-35, 106), galsim.PositionD(23.5, 55.1), galsim.Shear(e1=-0.1, e2=0.15))) out_cat.addRow([ 3.4560001, 8.e3, -4, 17.0, 1, 0, 'demised!', '"kicked', 'the', 'bucket"', 0.4 * galsim.radians, galsim.PositionI(88, 99), galsim.PositionD(-0.99, -0.88), galsim.Shear() ]) # First the ASCII version out_cat.write(dir='output', file_name='catalog.dat') cat = galsim.Catalog(dir='output', file_name='catalog.dat') np.testing.assert_equal(cat.ncols, 17) np.testing.assert_equal(cat.nobjects, 3) np.testing.assert_equal(cat.isFits(), False) np.testing.assert_almost_equal(cat.getFloat(1, 0), 2.345) np.testing.assert_almost_equal(cat.getFloat(2, 1), 8000.) np.testing.assert_equal(cat.getInt(0, 2), 9) np.testing.assert_equal(cat.getInt(2, 3), 17) np.testing.assert_equal(cat.getInt(2, 4), 1) np.testing.assert_equal(cat.getInt(0, 5), 1) np.testing.assert_equal(cat.get(2, 6), 'demised!') np.testing.assert_equal(cat.get(1, 7), '"bereft') np.testing.assert_equal(cat.get(0, 8), 'to') np.testing.assert_equal(cat.get(2, 9), 'bucket"') np.testing.assert_almost_equal(cat.getFloat(0, 10), 1.2 * galsim.degrees / galsim.radians) np.testing.assert_almost_equal(cat.getInt(1, 11), -35) np.testing.assert_almost_equal(cat.getInt(1, 12), 106) np.testing.assert_almost_equal(cat.getFloat(2, 13), -0.99) np.testing.assert_almost_equal(cat.getFloat(2, 14), -0.88) np.testing.assert_almost_equal(cat.getFloat(0, 15), 0.2) np.testing.assert_almost_equal(cat.getFloat(0, 16), 0.1) # Next the FITS version out_cat.write(dir='output', file_name='catalog.fits') cat = galsim.Catalog(dir='output', file_name='catalog.fits') np.testing.assert_equal(cat.ncols, 17) np.testing.assert_equal(cat.nobjects, 3) np.testing.assert_equal(cat.isFits(), True) np.testing.assert_almost_equal(cat.getFloat(1, 'float1'), 2.345) np.testing.assert_almost_equal(cat.getFloat(2, 'float2'), 8000.) np.testing.assert_equal(cat.getInt(0, 'int1'), 9) np.testing.assert_equal(cat.getInt(2, 'int2'), 17) np.testing.assert_equal(cat.getInt(2, 'bool1'), 1) np.testing.assert_equal(cat.getInt(0, 'bool2'), 1) np.testing.assert_equal(cat.get(2, 'str1'), 'demised!') np.testing.assert_equal(cat.get(1, 'str2'), '"bereft') np.testing.assert_equal(cat.get(0, 'str3'), 'to') np.testing.assert_equal(cat.get(2, 'str4'), 'bucket"') np.testing.assert_almost_equal(cat.getFloat(0, 'angle.rad'), 1.2 * galsim.degrees / galsim.radians) np.testing.assert_equal(cat.getInt(1, 'posi.x'), -35) np.testing.assert_equal(cat.getInt(1, 'posi.y'), 106) np.testing.assert_almost_equal(cat.getFloat(2, 'posd.x'), -0.99) np.testing.assert_almost_equal(cat.getFloat(2, 'posd.y'), -0.88) np.testing.assert_almost_equal(cat.getFloat(0, 'shear.g1'), 0.2) np.testing.assert_almost_equal(cat.getFloat(0, 'shear.g2'), 0.1) # Check pickling do_pickle(out_cat) out_cat2 = galsim.OutputCatalog(names, types) # No data. do_pickle(out_cat2) t2 = time.time() print 'time for %s = %.2f' % (funcname(), t2 - t1)
def test_operations_simple(): """Simple test of operations on InterpolatedImage: shear, magnification, rotation, shifting.""" import time t1 = time.time() # Make some nontrivial image that can be described in terms of sums and convolutions of # GSObjects. We want this to be somewhat hard to describe, but should be at least # critically-sampled, so put in an Airy PSF. gal_flux = 1000. pix_scale = 0.03 # arcsec bulge_frac = 0.3 bulge_hlr = 0.3 # arcsec bulge_e = 0.15 bulge_pos_angle = 30.*galsim.degrees disk_hlr = 0.6 # arcsec disk_e = 0.5 disk_pos_angle = 60.*galsim.degrees lam = 800 # nm NB: don't use lambda - that's a reserved word. tel_diam = 2.4 # meters lam_over_diam = lam * 1.e-9 / tel_diam # radians lam_over_diam *= 206265 # arcsec im_size = 512 # define subregion for comparison comp_region=30 # compare the central region of this linear size comp_bounds = galsim.BoundsI(1,comp_region,1,comp_region) comp_bounds = comp_bounds.shift(galsim.PositionI((im_size-comp_region)/2, (im_size-comp_region)/2)) bulge = galsim.Sersic(4, half_light_radius=bulge_hlr) bulge.applyShear(e=bulge_e, beta=bulge_pos_angle) disk = galsim.Exponential(half_light_radius = disk_hlr) disk.applyShear(e=disk_e, beta=disk_pos_angle) gal = bulge_frac*bulge + (1.-bulge_frac)*disk gal.setFlux(gal_flux) psf = galsim.Airy(lam_over_diam) pix = galsim.Pixel(pix_scale) obj = galsim.Convolve(gal, psf, pix) im = obj.draw(scale=pix_scale) # Turn it into an InterpolatedImage with default param settings int_im = galsim.InterpolatedImage(im) # Shear it, and compare with expectations from GSObjects directly test_g1=-0.07 test_g2=0.1 test_decimal=2 # in % difference, i.e. 2 means 1% agreement test_int_im = int_im.createSheared(g1=test_g1, g2=test_g2) ref_obj = obj.createSheared(g1=test_g1, g2=test_g2) # make large images im = galsim.ImageD(im_size, im_size) ref_im = galsim.ImageD(im_size, im_size) test_int_im.draw(image=im, scale=pix_scale) ref_obj.draw(image=ref_im, scale=pix_scale) # define subregion for comparison im_sub = im.subImage(comp_bounds) ref_im_sub = ref_im.subImage(comp_bounds) diff_im=im_sub-ref_im_sub rel = diff_im/im_sub zeros_arr = np.zeros((comp_region, comp_region)) # require relative difference to be smaller than some amount np.testing.assert_array_almost_equal(rel.array, zeros_arr, test_decimal, err_msg='Sheared InterpolatedImage disagrees with reference') # Magnify it, and compare with expectations from GSObjects directly test_mag = 1.08 test_decimal=2 # in % difference, i.e. 2 means 1% agreement comp_region=30 # compare the central region of this linear size test_int_im = int_im.createMagnified(test_mag) ref_obj = obj.createMagnified(test_mag) # make large images im = galsim.ImageD(im_size, im_size) ref_im = galsim.ImageD(im_size, im_size) test_int_im.draw(image=im, scale=pix_scale) ref_obj.draw(image=ref_im, scale=pix_scale) # define subregion for comparison im_sub = im.subImage(comp_bounds) ref_im_sub = ref_im.subImage(comp_bounds) diff_im=im_sub-ref_im_sub rel = diff_im/im_sub zeros_arr = np.zeros((comp_region, comp_region)) # require relative difference to be smaller than some amount np.testing.assert_array_almost_equal(rel.array, zeros_arr, test_decimal, err_msg='Magnified InterpolatedImage disagrees with reference') # Lens it (shear and magnify), and compare with expectations from GSObjects directly test_g1 = -0.03 test_g2 = -0.04 test_mag = 0.74 test_decimal=2 # in % difference, i.e. 2 means 1% agreement comp_region=30 # compare the central region of this linear size test_int_im = int_im.createLensed(test_g1, test_g2, test_mag) ref_obj = obj.createLensed(test_g1, test_g2, test_mag) # make large images im = galsim.ImageD(im_size, im_size) ref_im = galsim.ImageD(im_size, im_size) test_int_im.draw(image=im, scale=pix_scale) ref_obj.draw(image=ref_im, scale=pix_scale) # define subregion for comparison im_sub = im.subImage(comp_bounds) ref_im_sub = ref_im.subImage(comp_bounds) diff_im=im_sub-ref_im_sub rel = diff_im/im_sub zeros_arr = np.zeros((comp_region, comp_region)) # require relative difference to be smaller than some amount np.testing.assert_array_almost_equal(rel.array, zeros_arr, test_decimal, err_msg='Lensed InterpolatedImage disagrees with reference') # Rotate it, and compare with expectations from GSObjects directly test_rot_angle = 32.*galsim.degrees test_decimal=2 # in % difference, i.e. 2 means 1% agreement comp_region=30 # compare the central region of this linear size test_int_im = int_im.createRotated(test_rot_angle) ref_obj = obj.createRotated(test_rot_angle) # make large images im = galsim.ImageD(im_size, im_size) ref_im = galsim.ImageD(im_size, im_size) test_int_im.draw(image=im, scale=pix_scale) ref_obj.draw(image=ref_im, scale=pix_scale) # define subregion for comparison im_sub = im.subImage(comp_bounds) ref_im_sub = ref_im.subImage(comp_bounds) diff_im=im_sub-ref_im_sub rel = diff_im/im_sub zeros_arr = np.zeros((comp_region, comp_region)) # require relative difference to be smaller than some amount np.testing.assert_array_almost_equal(rel.array, zeros_arr, test_decimal, err_msg='Rotated InterpolatedImage disagrees with reference') # Shift it, and compare with expectations from GSObjects directly x_shift = -0.31 y_shift = 0.87 test_decimal=2 # in % difference, i.e. 2 means 1% agreement comp_region=30 # compare the central region of this linear size test_int_im = int_im.createShifted(x_shift, y_shift) ref_obj = obj.createShifted(x_shift, y_shift) # make large images im = galsim.ImageD(im_size, im_size) ref_im = galsim.ImageD(im_size, im_size) test_int_im.draw(image=im, scale=pix_scale) ref_obj.draw(image=ref_im, scale=pix_scale) # define subregion for comparison im_sub = im.subImage(comp_bounds) ref_im_sub = ref_im.subImage(comp_bounds) diff_im=im_sub-ref_im_sub rel = diff_im/im_sub zeros_arr = np.zeros((comp_region, comp_region)) # require relative difference to be smaller than some amount np.testing.assert_array_almost_equal(rel.array, zeros_arr, test_decimal, err_msg='Shifted InterpolatedImage disagrees with reference') t2 = time.time() print 'time for %s = %.2f'%(funcname(),t2-t1)
def get_blend_shape(mu, c, e1, e2, hlr, flux, hsm=HLRShearModel(), wcs=None, pixel_scale=None, return_hlr=False, return_moments=False, out_unit='pixel'): """ Returns the combined shear of the blended system Not quite ready for multiple blended systems! TODO! [assuming N galaxies in the blended system] #A : the array of total NORMALIZED fluxes mu : the array (N vectors) of galaxy centers (i.e. the peaks of the Gaussians) c : the vector pointing to the luminosity center of the blended system e1 : the array of the first component of the shears for N galaxies e2 : the array of the second component of the shears for N galaxies flux : the flux of galaxies in the blend (in whatever unit or zeropoint but consistent) hlr in arcsec and mu,c in degrees. returns Q_blend in pixel^2, hlr_blend in arcsec """ if wcs is None: cen_ra = 0.5 * (mu[0].max() + mu[0].min()) * galsim.degrees cen_dec = 0.5 * (mu[1].max() + mu[1].min()) * galsim.degrees cen_coord = galsim.CelestialCoord(cen_ra, cen_dec) #, gsparams=gsp) affine_wcs = galsim.PixelScale(pixel_scale).affine().withOrigin( galsim.PositionI(0, 0)) wcs = galsim.TanWCS(affine_wcs, world_origin=cen_coord) #, gsparams=gsp) mu = galsim_world2pix(wcs, mu[0], mu[1]) c = galsim_world2pix(wcs, [c[0]], [c[1]]) # assumes scalar c[0], c[1] Sigma = get_shape_covmat_fast( hlr / pixel_scale, e1, e2, hsm=hsm ) # an array filled with second moments tensors of the blend members A = flux / (2 * np.pi * np.linalg.det(Sigma)**0.5) # compute the second moments of the blend "system" (collectively) Q_blend = get_blend_moments(A, mu, c, Sigma, unit='pixel') hlr_blend, e1_blend, e2_blend = hlr_from_moments_fast(Q_blend, hsm=hsm, return_shape=True) to_return = [e1_blend, e2_blend] if out_unit.startswith('deg'): convertor = pixel_scale * 3600 elif out_unit.startswith('arcmin'): convertor = pixel_scale * 60 elif out_unit.startswith('arcsec'): convertor = pixel_scale elif out_unit.startswith('pix'): convertor = 1.0 else: raise RuntimeError('Invalid `out_unit`') if return_hlr: to_return += [hlr_blend * convertor] if return_moments: to_return += [Q_blend * convertor**2] return to_return
def BuildStamp(config, obj_num=0, xsize=0, ysize=0, do_noise=True, logger=None): """ Build a single stamp image using the given config file @param config A configuration dict. @param obj_num If given, the current obj_num [default: 0] @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 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] @returns the tuple (image, current_var) """ SetupConfigObjNum(config, obj_num) stamp = config['stamp'] stamp_type = stamp['type'] if stamp_type not in valid_stamp_types: raise AttributeError("Invalid stamp.type=%s." % stamp_type) builder = valid_stamp_types[stamp_type] # Add 1 to the seed here so the first object has a different rng than the file or image. seed = galsim.config.SetupConfigRNG(config, seed_offset=1) if logger: logger.debug('obj %d: seed = %d', obj_num, seed) if 'retry_failures' in stamp: ntries = galsim.config.ParseValue(stamp, '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 elif ('reject' in stamp or 'min_flux_frac' in stamp or 'min_snr' in stamp or 'max_snr' in stamp): # Still impose a maximum number of tries to prevent infinite loops. ntries = 20 else: ntries = 1 for itry in range(ntries): # 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. try: # Do the necessary initial setup for this stamp type. xsize, ysize, image_pos, world_pos = builder.setup( stamp, config, xsize, ysize, stamp_ignore, logger) # Save these values for possible use in Evals or other modules SetupConfigStampSize(config, xsize, ysize, image_pos, world_pos) stamp_center = config['stamp_center'] if logger: if xsize: logger.debug('obj %d: xsize,ysize = %s,%s', obj_num, xsize, ysize) if image_pos: logger.debug('obj %d: image_pos = %s', obj_num, image_pos) if world_pos: logger.debug('obj %d: world_pos = %s', obj_num, world_pos) if stamp_center: logger.debug('obj %d: stamp_center = %s', obj_num, stamp_center) # Get the global gsparams kwargs. Individual objects can add to this. gsparams = {} if 'gsparams' in stamp: gsparams = galsim.config.UpdateGSParams( gsparams, stamp['gsparams'], config) skip = False try: psf = galsim.config.BuildGSObject(config, 'psf', gsparams=gsparams, logger=logger)[0] prof = builder.buildProfile(stamp, config, psf, gsparams, logger) except galsim.config.gsobject.SkipThisObject as e: if logger: logger.debug('obj %d: Caught SkipThisObject: e = %s', obj_num, e.msg) if logger: if e.msg: # If there is a message, upgrade to info level logger.info('Skipping object %d: %s', obj_num, e.msg) skip = True # Note: Skip is different from Reject. # Skip means we return None for this stamp image and continue on. # Reject means we retry this object using the same obj_num. # This has implications for the total number of objects as well as # things like ring tests that rely on objects being made in pairs. # # Skip is also different from prof = None. # If prof is None, then the user indicated that no object should be # drawn on this stamp, but that a noise image is still desired. im = builder.makeStamp(stamp, config, xsize, ysize, logger) if not skip: if 'draw_method' in stamp: method = galsim.config.ParseValue(stamp, '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) offset = config['stamp_offset'] if 'offset' in stamp: offset += galsim.config.ParseValue(stamp, 'offset', config, galsim.PositionD)[0] if logger: logger.debug('obj %d: offset = %s', obj_num, offset) im = builder.draw(prof, im, method, offset, stamp, config, logger) scale_factor = builder.getSNRScale(im, stamp, config, logger) im, prof = builder.applySNRScale(im, prof, scale_factor, method, logger) # Set the origin appropriately if im is None: # Note: im might be None here if the stamp size isn't given and skip==True. pass elif stamp_center: im.setCenter(stamp_center) else: im.setOrigin(config.get('image_origin', galsim.PositionI(1, 1))) # Store the current stamp in the base-level config for reference config['current_stamp'] = im # This is also information that the weight image calculation needs config['do_noise_in_stamps'] = do_noise # Check if this object should be rejected. if not skip: reject = builder.reject(stamp, config, prof, psf, im, logger) if reject: if itry + 1 < ntries: if logger: logger.warning( 'Object %d: Rejecting this object and rebuilding', obj_num) builder.reset(config, logger) continue else: # pragma: no cover if logger: logger.error( 'Object %d: Too many rejections for this object. Aborting.', obj_num) raise RuntimeError( "Rejected an object %d times. If this is expected, " % ntries + "you should specify a larger stamp.retry_failures." ) galsim.config.ProcessExtraOutputsForStamp(config, logger) # We always need to do the whiten step here in the stamp processing if not skip: current_var = builder.whiten(prof, im, stamp, config, logger) if current_var != 0.: if logger: logger.debug( 'obj %d: whitening noise brought current var to %f', config['obj_num'], current_var) else: current_var = 0. # Sometimes, depending on the image type, we go on to do the rest of the noise as well. if do_noise: im, current_var = builder.addNoise(stamp, config, im, skip, current_var, logger) return im, current_var except KeyboardInterrupt: raise 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) if logger: import traceback tr = traceback.format_exc() logger.debug('obj %d: Traceback = %s', obj_num, tr) # Need to remove the "current_val"s from the config dict. Otherwise, # the value generators will do a quick return with the cached value. builder.reset(config, logger) continue
def SetupConfigStampSize(config, xsize, ysize, image_pos, world_pos): """Do further setup of the config dict at the stamp (or object) processing level reflecting the stamp size and position in either image or world coordinates. Includes: - If given, set config['stamp_xsize'] = xsize - If given, set config['stamp_ysize'] = ysize - If only image_pos or world_pos is given, compute the other from config['wcs'] - Set config['index_pos'] = image_pos - Set config['world_pos'] = world_pos - Calculate the appropriate value of the center of the stamp, to be used with the command: stamp_image.setCenter(stamp_center). Save this as config['stamp_center'] - Calculate the appropriate offset for the position of the object from the center of the stamp due to just the fractional part of the image position, not including any config['stamp']['offset'] item that may be present in the config dict. Save this as config['stamp_offset'] @param config A configuration dict. @param xsize The size of the stamp in the x-dimension. [may be None] @param ysize The size of the stamp in the y-dimension. [may be None] @param image_pos The position of the stamp in image coordinates. [may be None] @param world_pos The position of the stamp in world coordinates. [may be None] """ # Make sure we have a valid wcs in case image-level processing was skipped. if 'wcs' not in config: config['wcs'] = galsim.config.BuildWCS(config) if xsize: config['stamp_xsize'] = xsize if ysize: config['stamp_ysize'] = ysize if image_pos is not None and world_pos is None: # 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 is not None and image_pos is None: # Calculate and save the position relative to the image center image_pos = config['wcs'].toImage(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 stamp_center = galsim.PositionI(int(math.floor(nominal_x + 0.5)), int(math.floor(nominal_y + 0.5))) config['stamp_center'] = stamp_center config['stamp_offset'] = galsim.PositionD(nominal_x - stamp_center.x, nominal_y - stamp_center.y) config['image_pos'] = image_pos config['world_pos'] = world_pos else: config['stamp_center'] = None config['stamp_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.) config['world_pos'] = world_pos
def inject_psf(image, mag, coord, psf=None, seed=None): """Realize the DES_PSFEx PSF model `psf` at location `coord` on ZTF science image `image` (image path) with magnitude `mag` in the AB system, fluctuated by Poisson noise. """ # initialize the random number generator import galsim rng = galsim.BaseDeviate(seed) # handle both scalar and vector inputs mag = np.atleast_1d(mag) if coord.isscalar: coord = coord.reshape([1]) with fits.open(image, mode='update') as hdul: # read in the WCS header = hdul[0].header wcs = WCS(header=header) # measure the PSF using PSFEx if not already specified if psf is None: psf = measure_psf(image) # load the image into galsim gimage = galsim.fits.read(hdu_list=hdul) # convert the world coordinates to pixel coordinates ipos = wcs.all_world2pix([[pos.ra.deg, pos.dec.deg] for pos in coord], 1) for m, pos in zip(mag, ipos): # calculate the measured flux of the object flux = 10**(-0.4 * (m - header['MAGZP'])) image_pos = galsim.PositionD(*pos) # store the center of the nearest integer pixel iimage_pos = galsim.PositionI(*tuple(map(round, pos))) ix, iy = iimage_pos.x, iimage_pos.y # calculate the offset between the stamp center # and the profile center offset = image_pos - iimage_pos # create an output stamp onto which to draw the fluctuated # psf bounds = galsim.BoundsI(ix - NPIX, ix + NPIX, iy - NPIX, iy + NPIX) # check that there is at least some overlap between the bounds = bounds & gimage.bounds if not bounds.isDefined(): raise RuntimeError('No overlap between PSF stamp and image. ' 'Is the object coordinate contained by the ' 'image?') # get the noise noise = galsim.PoissonNoise(rng) # realize the psf at the coordinates realization = psf.getPSF(image_pos).withFlux(flux) # get the local wcs lwcs = psf.getLocalWCS(image_pos) # draw the image imout = realization.drawImage(wcs=lwcs, offset=offset, nx=NPIX * 2 + 1, ny=NPIX * 2 + 1) # add the noise imout.addNoise(noise) # shift the image to the right spot imout.setCenter(iimage_pos) # add the photons gimage[bounds] = gimage[bounds] + imout[bounds] # save it as a new hdu galsim.fits.write(gimage, hdu_list=hdul) # propagate original WCS to output extension wcskeys = wcs.to_header(relax=True) hdul[-1].header.update(wcskeys) # add a record of the fakes as a bintable record = { 'fake_mag': mag, 'fake_ra': coord.ra.deg, 'fake_dec': coord.dec.deg, 'fake_x': ipos[:, 0], 'fake_y': ipos[:, 1] } # save the bintable as an extension table = Table(record) nhdu = fits.BinTableHDU(table.as_array()) hdul.append(nhdu)
def determinePsf(self, exposure, psfCandidateList, metadata=None, flagKey=None): """Determine a Piff PSF model for an exposure given a list of PSF candidates. Parameters ---------- exposure : `lsst.afw.image.Exposure` Exposure containing the PSF candidates. psfCandidateList : `list` of `lsst.meas.algorithms.PsfCandidate` A sequence of PSF candidates typically obtained by detecting sources and then running them through a star selector. metadata : `lsst.daf.base import PropertyList` or `None`, optional A home for interesting tidbits of information. flagKey : `str` or `None`, optional Schema key used to mark sources actually used in PSF determination. Returns ------- psf : `lsst.meas.extensions.piff.PiffPsf` The measured PSF model. psfCellSet : `None` Unused by this PsfDeterminer. """ stars = [] for candidate in psfCandidateList: cmi = candidate.getMaskedImage() weight = computeWeight(cmi, self.config.maxSNR) bbox = cmi.getBBox() bds = galsim.BoundsI(galsim.PositionI(*bbox.getMin()), galsim.PositionI(*bbox.getMax())) gsImage = galsim.Image(bds, scale=1.0, dtype=float) gsImage.array[:] = cmi.image.array gsWeight = galsim.Image(bds, scale=1.0, dtype=float) gsWeight.array[:] = weight source = candidate.getSource() image_pos = galsim.PositionD(source.getX(), source.getY()) data = piff.StarData(gsImage, image_pos, weight=gsWeight) stars.append(piff.Star(data, None)) kernelSize = int( np.clip(self.config.kernelSize, self.config.kernelSizeMin, self.config.kernelSizeMax)) piffConfig = { 'type': "Simple", 'model': { 'type': 'PixelGrid', 'scale': self.config.samplingSize, 'size': kernelSize }, 'interp': { 'type': 'BasisPolynomial', 'order': self.config.spatialOrder }, 'outliers': { 'type': 'Chisq', 'nsigma': self.config.outlierNSigma, 'max_remove': self.config.outlierMaxRemove } } piffResult = piff.PSF.process(piffConfig) # Run on a single CCD, and in image coords rather than sky coords. wcs = {0: galsim.PixelScale(1.0)} pointing = None logger = logging.getLogger(self.log.getName() + ".Piff") logger.addHandler(lsst.log.LogHandler()) piffResult.fit(stars, wcs, pointing, logger=logger) psf = PiffPsf(kernelSize, kernelSize, piffResult) used_image_pos = [s.image_pos for s in piffResult.stars] if flagKey: for candidate in psfCandidateList: source = candidate.getSource() posd = galsim.PositionD(source.getX(), source.getY()) if posd in used_image_pos: source.set(flagKey, True) if metadata is not None: metadata.set("spatialFitChi2", piffResult.chisq) metadata.set("numAvailStars", len(stars)) metadata.set("numGoodStars", len(piffResult.stars)) metadata.set("avgX", np.mean([p.x for p in piffResult.stars])) metadata.set("avgY", np.mean([p.y for p in piffResult.stars])) return psf, None
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_output_catalog(): """Test basic operations on Catalog.""" names = [ 'float1', 'float2', 'int1', 'int2', 'bool1', 'bool2', 'str1', 'str2', 'str3', 'str4', 'angle', 'posi', 'posd', 'shear' ] types = [ float, 'f8', int, 'i4', bool, 'bool', str, 'str', 'S', 'S0', galsim.Angle, galsim.PositionI, galsim.PositionD, galsim.Shear ] out_cat = galsim.OutputCatalog(names, types) row1 = (1.234, 4.131, 9, -3, 1, True, "He's", '"ceased', 'to', 'be"', 1.2 * galsim.degrees, galsim.PositionI(5, 6), galsim.PositionD(0.3, -0.4), galsim.Shear(g1=0.2, g2=0.1)) row2 = (2.345, -900, 0.0, 8, False, 0, "bleedin'", '"bereft', 'of', 'life"', 11 * galsim.arcsec, galsim.PositionI(-35, 106), galsim.PositionD(23.5, 55.1), galsim.Shear(e1=-0.1, e2=0.15)) row3 = (3.4560001, 8.e3, -4, 17.0, 1, 0, 'demised!', '"kicked', 'the', 'bucket"', 0.4 * galsim.radians, galsim.PositionI(88, 99), galsim.PositionD(-0.99, -0.88), galsim.Shear()) out_cat.addRow(row1) out_cat.addRow(row2) out_cat.addRow(row3) assert out_cat.names == out_cat.getNames() == names assert out_cat.types == out_cat.getTypes() == types assert len(out_cat) == out_cat.getNObjects() == out_cat.nobjects == 3 assert out_cat.getNCols() == out_cat.ncols == len(names) # Can also set the types after the fact. # MJ: I think this used to be used by the "truth" catalog extra output. # But it doesn't seem to be used there anymore. Probably not by anything then. # I'm not sure how useful it is, I guess it doesn't hurt to leave it in. out_cat2 = galsim.OutputCatalog(names) assert out_cat2.types == [float] * len(names) out_cat2.setTypes(types) assert out_cat2.types == out_cat2.getTypes() == types # Another feature that doesn't seem to be used anymore is you can add the rows out of order # and just give a key to use for sorting at the end. out_cat2.addRow(row3, 3) out_cat2.addRow(row1, 1) out_cat2.addRow(row2, 2) # Check ASCII round trip out_cat.write(dir='output', file_name='catalog.dat') cat = galsim.Catalog(dir='output', file_name='catalog.dat') np.testing.assert_equal(cat.ncols, 17) np.testing.assert_equal(cat.nobjects, 3) np.testing.assert_equal(cat.isFits(), False) np.testing.assert_almost_equal(cat.getFloat(1, 0), 2.345) np.testing.assert_almost_equal(cat.getFloat(2, 1), 8000.) np.testing.assert_equal(cat.getInt(0, 2), 9) np.testing.assert_equal(cat.getInt(2, 3), 17) np.testing.assert_equal(cat.getInt(2, 4), 1) np.testing.assert_equal(cat.getInt(0, 5), 1) np.testing.assert_equal(cat.get(2, 6), 'demised!') np.testing.assert_equal(cat.get(1, 7), '"bereft') np.testing.assert_equal(cat.get(0, 8), 'to') np.testing.assert_equal(cat.get(2, 9), 'bucket"') np.testing.assert_almost_equal(cat.getFloat(0, 10), 1.2 * galsim.degrees / galsim.radians) np.testing.assert_almost_equal(cat.getInt(1, 11), -35) np.testing.assert_almost_equal(cat.getInt(1, 12), 106) np.testing.assert_almost_equal(cat.getFloat(2, 13), -0.99) np.testing.assert_almost_equal(cat.getFloat(2, 14), -0.88) np.testing.assert_almost_equal(cat.getFloat(0, 15), 0.2) np.testing.assert_almost_equal(cat.getFloat(0, 16), 0.1) # Check FITS round trip out_cat.write(dir='output', file_name='catalog.fits') cat = galsim.Catalog(dir='output', file_name='catalog.fits') np.testing.assert_equal(cat.ncols, 17) np.testing.assert_equal(cat.nobjects, 3) np.testing.assert_equal(cat.isFits(), True) np.testing.assert_almost_equal(cat.getFloat(1, 'float1'), 2.345) np.testing.assert_almost_equal(cat.getFloat(2, 'float2'), 8000.) np.testing.assert_equal(cat.getInt(0, 'int1'), 9) np.testing.assert_equal(cat.getInt(2, 'int2'), 17) np.testing.assert_equal(cat.getInt(2, 'bool1'), 1) np.testing.assert_equal(cat.getInt(0, 'bool2'), 1) np.testing.assert_equal(cat.get(2, 'str1'), 'demised!') np.testing.assert_equal(cat.get(1, 'str2'), '"bereft') np.testing.assert_equal(cat.get(0, 'str3'), 'to') np.testing.assert_equal(cat.get(2, 'str4'), 'bucket"') np.testing.assert_almost_equal(cat.getFloat(0, 'angle.rad'), 1.2 * galsim.degrees / galsim.radians) np.testing.assert_equal(cat.getInt(1, 'posi.x'), -35) np.testing.assert_equal(cat.getInt(1, 'posi.y'), 106) np.testing.assert_almost_equal(cat.getFloat(2, 'posd.x'), -0.99) np.testing.assert_almost_equal(cat.getFloat(2, 'posd.y'), -0.88) np.testing.assert_almost_equal(cat.getFloat(0, 'shear.g1'), 0.2) np.testing.assert_almost_equal(cat.getFloat(0, 'shear.g2'), 0.1) # The one that was made out of order should write the same file. out_cat2.write(dir='output', file_name='catalog2.fits') cat2 = galsim.Catalog(dir='output', file_name='catalog2.fits') np.testing.assert_array_equal(cat2.data, cat.data) assert cat2 != cat # Because file_name is different. # Check that it properly overwrites an existing output file. out_cat.addRow([ 1.234, 4.131, 9, -3, 1, True, "He's", '"ceased', 'to', 'be"', 1.2 * galsim.degrees, galsim.PositionI(5, 6), galsim.PositionD(0.3, -0.4), galsim.Shear(g1=0.2, g2=0.1) ]) assert out_cat.rows[3] == out_cat.rows[0] out_cat.write(dir='output', file_name='catalog.fits') # Same name as above. cat2 = galsim.Catalog(dir='output', file_name='catalog.fits') np.testing.assert_equal(cat2.ncols, 17) np.testing.assert_equal(cat2.nobjects, 4) for key in names[:10]: assert cat2.data[key][3] == cat2.data[key][0] # Check pickling do_pickle(out_cat) out_cat2 = galsim.OutputCatalog(names, types) # No data. do_pickle(out_cat2) # Check errors with assert_raises(galsim.GalSimValueError): out_cat.addRow((1, 2, 3)) # Wrong length with assert_raises(galsim.GalSimValueError): out_cat.write(dir='output', file_name='catalog.txt', file_type='invalid')