Source code for galsim.config.extra_psf
# Copyright (c) 2012-2023 by the GalSim developers team on GitHub
# https://github.com/GalSim-developers
#
# This file is part of GalSim: The modular galaxy image simulation toolkit.
# https://github.com/GalSim-developers/GalSim
#
# GalSim is free software: redistribution and use in source and binary forms,
# with or without modification, are permitted provided that the following
# conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this
# list of conditions, and the disclaimer given in the accompanying LICENSE
# file.
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the disclaimer given in the documentation
# and/or other materials provided with the distribution.
#
import math
import numpy as np
import logging
from .extra import ExtraOutputBuilder, RegisterExtraOutput
from .stamp import valid_draw_methods
from .value import ParseValue, GetCurrentValue
from .util import GetRNG
from .noise import CalculateNoiseVariance, AddNoise
from ..image import Image
from ..position import PositionD
from ..errors import GalSimConfigValueError, GalSimConfigError
# The psf extra output type builds an Image of the PSF at the same locations as the galaxies.
# The code the actually draws the PSF on a postage stamp.
def DrawPSFStamp(psf, config, base, bounds, offset, method, logger):
"""
Draw an image using the given psf profile.
Returns:
the resulting image.
"""
if 'draw_method' in config:
method = ParseValue(config,'draw_method',base,str)[0]
if method not in valid_draw_methods:
raise GalSimConfigValueError("Invalid draw_method.", method, valid_draw_methods)
else:
method = 'auto'
if 'flux' in config:
flux = ParseValue(config,'flux',base,float)[0]
psf = psf.withFlux(flux)
if method == 'phot':
rng = GetRNG(config, base)
n_photons = psf.flux
else:
rng = None
n_photons = 0
wcs = base['wcs'].local(base['image_pos'])
im = Image(bounds, wcs=wcs, dtype=base['current_stamp'].dtype)
im = psf.drawImage(image=im, offset=offset, method=method, rng=rng, n_photons=n_photons)
if 'signal_to_noise' in config:
if 'flux' in config:
raise GalSimConfigError(
"Cannot specify both flux and signal_to_noise for psf output")
if method == 'phot':
raise GalSimConfigError(
"signal_to_noise option not implemented for draw_method = phot")
if 'image' in base and 'noise' in base['image']:
noise_var = CalculateNoiseVariance(base)
else:
raise GalSimConfigError(
"Need to specify noise level when using psf.signal_to_noise")
sn_target = ParseValue(config, 'signal_to_noise', base, float)[0]
sn_meas = np.sqrt( np.sum(im.array**2, dtype=float) / noise_var )
flux = sn_target / sn_meas
im *= flux
return im
# The function to call at the end of building each stamp
[docs]class ExtraPSFBuilder(ExtraOutputBuilder):
"""Build an image that draws the PSF at the same location as each object on the main image.
This makes the most sense when the main image consists of non-overlapping stamps, such as
a TiledImage, since you wouldn't typically want the PSF images to overlap. But it just
follows whatever pattern of stamp locations the main image has.
"""
def processStamp(self, obj_num, config, base, logger):
# If this doesn't exist, an appropriate exception will be raised.
psf = base['psf']['current'][0]
draw_method = GetCurrentValue('draw_method', base['stamp'], str, base)
bounds = base['current_stamp'].bounds
# Check if we should shift the psf:
if 'shift' in config:
# Special: output.psf.shift = 'galaxy' means use the galaxy shift.
if config['shift'] == 'galaxy':
# This shift value might be in either stamp or gal.
b = base['stamp'] if 'shift' in base['stamp'] else base['gal']
# This will raise an appropriate error if there is no gal.shift or stamp.shift.
shift = GetCurrentValue('shift', b, PositionD, base)
else:
shift = ParseValue(config, 'shift', base, PositionD)[0]
logger.debug('obj %d: psf shift: %s',base.get('obj_num',0),str(shift))
psf = psf.shift(shift)
# Start with the offset required just due to the stamp size/shape.
offset = base['stamp_offset']
# Check if we should apply any additional offset:
if 'offset' in config:
# Special: output.psf.offset = 'galaxy' means use the same offset as in the galaxy
# image, which is actually in config.stamp, not config.gal.
if config['offset'] == 'galaxy':
offset += GetCurrentValue('offset', base['stamp'], PositionD, base)
else:
offset += ParseValue(config, 'offset', base, PositionD)[0]
logger.debug('obj %d: psf offset: %s',base.get('obj_num',0),str(offset))
psf_im = DrawPSFStamp(psf,config,base,bounds,offset,draw_method,logger)
if 'signal_to_noise' in config:
base['current_noise_image'] = base['current_stamp']
AddNoise(base,psf_im,current_var=0,logger=logger)
self.scratch[obj_num] = psf_im
# The function to call at the end of building each image
def processImage(self, index, obj_nums, config, base, logger):
image = Image(base['image_bounds'], wcs=base['wcs'], init_value=0.,
dtype=base['current_image'].dtype)
# 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
logger.debug('image %d: psf image at b = %s = %s & %s',
base['image_num'],b,stamp.bounds,image.bounds)
if b.isDefined(): # pragma: no branch (We normally guard against this already.)
image[b] += stamp[b]
logger.debug('obj %d: added psf image to main image',base.get('obj_num',0))
self.data[index] = image
# Register this as a valid extra output
RegisterExtraOutput('psf', ExtraPSFBuilder())