Config Stamp - GalSim-developers/GalSim GitHub Wiki

Stamp Field Attributes

The stamp field defines some properties about how to draw the postage-stamp images of each object. It is often unneccessary to explicitly include this top-level field. The default stamp type, called 'Basic', is often what you want.

Some attributes that are allowed for all stamp types are:

  • draw_method = str_value (default = 'auto') Valid options are:
    • 'auto' The default is normally equivalent to 'fft'. However, if the object being rendered is simple (no convolution) and has hard edges (e.g. a Box or a truncated Moffat or Sersic), then it will switch to 'real_space', since that is often both faster and more accurate in these cases (due to ringing in Fourier space).
    • 'fft' This method will convolve by the pixel (as well as convolving the galaxy and PSF if you have both) using a fast Fourier transform.
    • 'real_space' This uses real-space integration to integrate over the pixel. This is only possible if there is only either a PSF or galaxy, not both. Also, it cannot involve a Convolution internally. If GalSim is unable to do the real-space integration, this will revert to 'fft'.
    • 'phot' Use photon shooting, which treats the profile as a probability distribution, draws photons from that distribution, and then "shoots" them at the image. The flux of each photon is added to whichever pixel it hits. This automatically handles the integration over the pixel, but it cannot be used with Deconvolutions (including RealGalaxy objects) and the result will necessarily have Poisson noise from the finite number of photons being shot.
      • max_extra_noise = float_value (optional) If the image is sky noise dominated, then it is efficient to stop shooting photons when the photon noise of the galaxy is much less than the sky noise. This parameter specifies how much extra noise, as a fraction of the sky noise, is permissible in any pixel.
      • n_photons = int_value (optional; default is to assume the object flux is given in photons and use that) Specifies the total number of photons to shoot as a hard, fixed number. If both n_photons and max_extra_noise are specified in the options, max_extra_noise is ignored and a warning is generated.
      • poisson_flux = bool_value (default = True, unless n_photons is given, in which case the default is False) Whether to allow the total object flux to vary according to Poisson statistics for the number of photons being shot.
    • 'no_pixel' This will not integrate the flux over the pixel response. Rather, it just samples the surface brightness distribution at the pixel centers and multiplies by the pixel area. This is appropriate if the PSF already has the pixel response included (e.g. from an observed image of a PSF).
    • 'sb' This is similar to 'no_pixel', except that the image values will simply be the sampled surface brightness, not multiplied by the pixel area. This does not correspond to any real observing scenario, but it could be useful if you want to view the surface brightness profile of an object directly, without including the pixel integration.
  • offset = pos_value (optional) An offset in chip coordinates (i.e. pixel units) to apply when drawing the object on the postage stamp.
  • gsparams = dict (optional) A dict of (non-default) GSParams items that you want applied to the constructed object.
  • retry_failures = int_value (default = 0) How many times to retry the construction of a GSObject if there is any kind of failure. For example, you might have a random shear value that technically may come back with |g| > 1, but it should be very rare. So you might set it to retry once or twice in that case. If this is > 0, then after a failure, the code will wait 1 second (in case the failure was related to memory usage on the machine), and then try again up to this many times.
  • world_pos = pos_value or sky_value (only one of world_pos and image_pos is allowed) The position in world coordinates at which to center the object. This is often defined in the image field, but it can be overridden in the stamp field.
  • image_pos = pos_value (only one of world_pos and image_pos is allowed) The position on the full image at which to center the object. This is often defined in the image field, but it can be overridden in the stamp field. Note: the object is always centered as nearly as possible on the postage stamp being drawn (unless an explicit offset is given), but the image_pos or world_pos determines where in the larger image this stamp is placed.
  • skip = bool_value (default=False) Skip this stamp.

Stamp Types

The default stamp type is 'Basic', which constructs a galaxy object based on the gal field (if present) and a PSF object from the psf field (again, if present), convolves them together, and draws the object onto a postage stamp. This is often what you need, but there is also a 'Ring' type, and you can define your own custom stamp type if you want to customize any aspect of the stamp-building process.

  • 'Basic' The postage stamp contains a single gal object convolved by a psf object, assuming both fields are given. If only one of the two is given, that one is drawn.
    • size = int_value (optional) If you want square postage stamps for each object (common), you just need to set this one value and the images will be size x size. The default is for GalSim to automatically determine a good size for the image that will encompass most of the flux of the object, but note that the image type may define the stamp size (e.g. 'Tiled'), in which case that will be used.
    • xsize = int_value (default = size) If you want non-square postage stamps, you can specify xsize and ysize separately instead. It is an error for only one of them to be non-zero.
    • ysize = int_value (default = size)
    • min_flux_frac = float_value (optional) If the rendered stamp (before noise is applied) has less than this fraction of the nominal flux of the object, reject it and start over (presumably choosing new random values for size, flux, etc.). This counts as a "failure" for the purpose of the retry_failures count.
    • min_snr = float_value (optional) If the measured signal-to-noise ratio (using the optimal matched filter definition of S/N, measured using the signal on the stamp before noise is applied) is less than this, then reject it and start over. This counts as a "failure" for the purpose of the retry_failures count.
    • max_snr = float_value (optional) If the measured signal-to-noise ratio is higher than this, then reject it and start over. This counts as a "failure" for the purpose of the retry_failures count.
    • reject = bool_value (optional) If this evaluates to true, then reject the current stamp and start over. Typically, this would be a custom function that would perform some measurement on the pre-noise image. See cgc.yaml and test_config_image.py for examples of such custom functions. This counts as a "failure" for the purpose of the retry_failures count.
  • 'Ring' Generate galaxies in a ring for a ring test. (Use num=2 to get pairs of 90 degree rotated galaxies.)
    • size, xsize, ysize = int_value (optional) Same meaning as for 'Basic' type.
    • num = int_value (required) How many objects to include in the ring.
    • full_rotation = angle_value (default = 180 degrees) What angle should be spanned by the full rotation? The default of 180 degrees is appropriate for the typical case of a rotationally symmetric galaxy (e.g. a sheared Exponential), but if the first profile does not have rotational symmetry, then you probably want to set this to 360 degrees.
    • index = int_value (default = 'Sequence' from 0 to num-1) Which item in the Ring is this.
    • min_flux_frac = float_value (optional) Equivalent to Basic, but only applies to the first stamp in the ring.
    • min_snr = float_value (optional) Equivalent to Basic, but only applies to the first stamp in the ring.
    • max_snr = float_value (optional) Equivalent to Basic, but only applies to the first stamp in the ring.
    • reject = bool_value (optional) Equivalent to Basic, but only applies to the first stamp in the ring.

Custom Stamp Types

To define your own stamp type, you will need to write an importable Python module (typically a file in the current directory where you are running galsim, but it could also be something you have installed in your Python distro) with a class that will be used to build the stamp.

The class should be a subclass of galsim.config.StampBuilder, which is the class used for the default 'Basic' type. There are a number of class methods, and you only need to override the ones for which you want different behavior than that of the 'Basic' type.

class CustomStampBuilder(galsim.config.StampBuilder):
    def setup(self, config, base, xsize, ysize, ignore, logger):
        """
        Do the initialization and setup for building a postage stamp.

        In the base class, we check for and parse the appropriate size and position values in
        config (aka base['stamp'] or base['image'].

        Values given in base['stamp'] take precedence if these are given in both places (which
        would be confusing, so probably shouldn't do that, but there might be a use case where it
        would make sense).

        @param config       The configuration dict for the stamp field.
        @param base         The base configuration dict.
        @param xsize        The xsize of the image to build (if known).
        @param ysize        The ysize of the image to build (if known).
        @param ignore       A list of parameters that are allowed to be in config that we can
                            ignore here. i.e. it won't be an error if these parameters are present.
        @param logger       If given, a logger object to log progress.

        @returns xsize, ysize, image_pos, world_pos
        """
        # .. Do any custom setup you need to do.
        # Probably want to call the base class setup function to do the normal determination
        # of the size and position values.
        return super(self.__class__, self).setup(config, base, xsize, ysize, ignore, logger)

    def buildProfile(self, config, base, psf, gsparams, logger):
        """Build the surface brightness profile (a GSObject) to be drawn.
 
        For the Basic stamp type, this builds a galaxy from the base['gal'] dict and convolves
        it with the psf (if given).  If either the psf or the galaxy is None, then the other one
        is returned as is.

        @param config       The configuration dict for the stamp field.
        @param base         The base configuration dict.
        @param psf          The PSF, if any.  This may be None, in which case, no PSF is convolved.
        @param gsparams     A dict of kwargs to use for a GSParams.  More may be added to this
                            list by the galaxy object.
        @param logger       If given, a logger object to log progress.

        @returns the final profile
        """
        # ... build the profile to draw
        # Note: this return value is only used by methods of the StampBuilder, so it can be
        # any type of object you want, not necessarily a GSObject, so long as you override the
        # other functions that take a prof parameter.
        return prof

    def makeStamp(self, config, base, xsize, ysize, logger):
        """Make the initial empty postage stamp image, if possible.

        If we don't know xsize, ysize, return None, in which case the stamp will be created
        automatically by the drawImage command based on the natural size of the profile.

        @param config       The configuration dict for the stamp field.
        @param base         The base configuration dict.
        @param xsize        The xsize of the image to build (if known).
        @param ysize        The ysize of the image to build (if known).
        @param logger       If given, a logger object to log progress.

        @returns the image (or None)
        """
        # ... build a blank image onto which the profile will be drawn, if possible.
        # The base class implementation for reference:
        if xsize and ysize:
            im = galsim.ImageF(xsize, ysize)
            im.setZero()
            return im
        else:
            return None

    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.

        @param prof         The profile to draw.
        @param image        The image onto which to draw the profile (which may be None).
        @param method       The method to use in drawImage.
        @param offset       The offset to apply when drawing.
        @param config       The configuration dict for the stamp field.
        @param base         The base configuration dict.
        @param logger       If given, a logger object to log progress.

        @returns whether to skip drawing this object.
        """
        # The base class checks if the object is completely off the image.  You probably want
        # to keep that functionality, but maybe you have additional reasons you might want to
        # skip the object.
        if super(CustomStampBuilder, self).updateSkip(prof, image, method offset, config, base, logger):
            return True

        # Check for some other reasons...
        if ...:
            return True

        return False

    def draw(self, prof, image, method, offset, config, base, logger):
        """Draw the profile on the postage stamp image.

        @param prof         The profile to draw.
        @param image        The image onto which to draw the profile (which may be None).
        @param method       The method to use in drawImage.
        @param offset       The offset to apply when drawing.
        @param config       The configuration dict for the stamp field.
        @param base         The base configuration dict.
        @param logger       If given, a logger object to log progress.

        @returns the resulting image
        """
        # ... draw prof onto the given image (making a new Image if necessary)
        return image

    def whiten(self, prof, image, config, base, logger):
        """If appropriate, whiten the resulting image according to the requested noise profile
        and the amount of noise originally present in the profile.

        @param prof         The profile to draw.
        @param image        The image onto which to draw the profile.
        @param config       The configuration dict for the stamp field.
        @param base         The base configuration dict.
        @param logger       If given, a logger object to log progress.

        @returns the variance of the resulting whitened (or symmetrized) image.
        """
        # ... whiten (or symmetrize) the noise in the image.
        # You will typically want to use the base class implementation and not override this.
        return current_var

    def getSNRScale(self, image, config, base, logger):
        """Calculate the factor by which to rescale the image based on a desired S/N level.

        @param image        The current image.
        @param config       The configuration dict for the stamp field.
        @param base         The base configuration dict.
        @param logger       If given, a logger object to log progress.

        @returns scale_factor
        """
        # ... calculate an appropriate scale factor by which to scale the image to get to the
        # desired signal-to-noise.
        # You will typically want to use the base class implementation and not override this.
        return scale_factor

    def applySNRScale(self, image, prof, scale_factor, method, logger):
        """Apply the scale_factor from getSNRScale to the image and profile.

        The default implementaion just multiplies each of them, but if prof is not a regular
        GSObject, then you might need to do something different.

        @param image        The current image.
        @param prof         The profile that was drawn.
        @param scale_factor The factor by which to scale both image and prof.
        @param method       The method used by drawImage.
        @param logger       If given, a logger object to log progress.

        @returns image, prof  (after being properly scaled)
        """
        # ... scale the image and prof objects.
        # The default implemenation is probably fine here unless your builder uses something
        # other than a GSObject for the prof object, in which case, you might need to do something
        # different here.
        # The base class implementation for reference:
        if scale_factor != 1.0:
            if method == 'phot':
                logger.warn("signal_to_noise calculation is not accurate for draw_method = phot")
            image *= scale_factor
            prof *= scale_factor
        return image, prof

    def reject(self, config, base, prof, psf, image, logger):
        """Check to see if this object should be rejected.

        @param config       The configuration dict for the stamp field.
        @param base         The base configuration dict.
        @param prof         The profile that was drawn.
        @param psf          The psf that was used to build the profile.
        @param image        The postage stamp image.  No noise is on it yet at this point.
        @param logger       If given, a logger object to log progress.

        @returns whether to reject this object
        """
        # The default implementation checks for reject, min_flux_frac, min_snr, and max_snr.
        # If you want to keep those checks in addition to something new, you can just run
        # the base class version first.
        reject = super(self.__class__,self).reject(config, base, prof, psf, image, logger)
        # ... Add any additional rejection checks
        return reject

    def reset(self, base, logger):
        """Reset some aspects of the config dict so the object can be rebuilt after rejecting the
        current object.

        @param base         The base configuration dict.
        @param logger       If given, a logger object to log progress.
        """
        # The base class implementation for reference:
        for field in ['psf', 'gal', 'stamp']:
            galsim.config.RemoveCurrent(base[field], keep_safe=True, index_key='obj_num')
        # If you use another field besides these three, you probably want to do the same thing
        # on that as well.

    def addNoise(self, config, base, image, skip, current_var, logger):
        """
        Add the sky level and the noise to the stamp.

        Note: This only gets called if the image type requests that the noise be added to each
            stamp individually, rather than to the full image and the end.

        @param config       The configuration dict for the stamp field.
        @param base         The base configuration dict.
        @param image        The current image.
        @param skip         Are we skipping this image?  (Usually this is irrelevant, since we
                            need sky and noise regardless, but user-defined classes might choose
                            to do something different if skipping this object.)
        @param current_var  The current noise variance present in the image already.
        @param logger       If given, a logger object to log progress.

        @returns the new values of image, current_var
        """
        # The base class implementation for reference:
        galsim.config.AddSky(base,image)
        base['current_noise_image'] = base['current_stamp']
        current_var = galsim.config.AddNoise(base,image,current_var,logger)
        return image, current_var

    def makeTasks(self, config, base, jobs, logger):
        """Turn a list of jobs into a list of tasks.

        For the Basic stamp type, there is just one job per task, so the tasks list is just:

            tasks = [ [ (job, k) ] for k, job in enumerate(jobs) ]

        @param config       The configuration dict for the stamp field.
        @param base         The base configuration dict.
        @param jobs         A list of jobs to split up into tasks.  Each job in the list is a
                            dict of parameters that includes 'obj_num'.
        @param logger       If given, a logger object to log progress.

        @returns a list of tasks
        """
        # The default implemenatation just creates a list with one item for each task.
        # If you need to make sure some consecutive postage stamps are done together in the 
        # same process, group them together in the list for each task.
        # The tuples in each task list consist of the job and its index into the original list.
        tasks = [ [ (job, k) ] for k, job in enumerate(jobs) ]
        return tasks

The base parameter is the original full configuration dict that is being used for running the simulation. The config parameter is the local portion of the full dict that defines the stamp being built, which would typically be base['stamp'].

Then, in the Python module, you need to register this function with some type name, which will be the value of the type attribute that triggers the use of this Builder object.

galsim.config.RegisterStampType('CustomStamp', CustomStampBuilder())

Note that we register an instance of the class, not the class itself. This opens up the possibility of having multiple stamp types use the same class instantiated with different initialization parameters. This is not used by the GalSim stamp types, but there may be use cases where it would be useful for custom stamp types.

Finally, to use this custom type in your config file, you need to tell the config parser the name of the module to load at the start of processing. e.g. if this function is defined in the file my_custom_stamp.py, then you would use the following top-level modules field in the config file:

modules:
    - my_custom_stamp

This modules field is a list, so it can contain more than one module to load if you want. Then before processing anything, the code will execute the command import my_custom_stamp, which will read your file and execute the registration command to add the buidler to the list of valid stamp types.

Then you can use this as a valid stamp type:

stamp:
    type: CustomStamp
    ...

For examples of custom stamps, see blend.yaml and blendset.yaml in the des examples directory, which use custom stamp types Blend and BlendSet defined in blend.py. It may also be helpful to look at the GalSim implementation of the Ring type.