Ejemplo n.º 1
0
    def build_file(seed, file_name, mass, nobj, rng, truth_file_name, halo_id,
                   first_obj_id):
        """A function that does all the work to build a single file.
           Returns the total time taken.
        """
        t1 = time.time()

        # Build the image onto which we will draw the galaxies.
        full_image = galsim.ImageF(image_size, image_size)

        # The "true" center of the image is allowed to be halfway between two pixels, as is the
        # case for even-sized images.  full_image.bounds.center() is an integer position,
        # which would be 1/2 pixel up and to the right of the true center in this case.
        im_center = full_image.bounds.trueCenter()

        # For the WCS, this time we use UVFunction, which lets you define arbitrary u(x,y)
        # and v(x,y) functions.  We use a simple cubic radial function to create a
        # pincushion distortion.  This is a typical kind of telescope distortion, although
        # we exaggerate the magnitude of the effect to make it more apparent.
        # The pixel size in the center of the image is 0.05, but near the corners (r=362),
        # the pixel size is approximately 0.075, which is much more distortion than is
        # normally present in typical telescopes.  But it makes the effect of the variable
        # pixel area obvious when you look at the weight image in the output files.
        ufunc1 = lambda x, y: 0.05 * x * (1. + 2.e-6 * (x**2 + y**2))
        vfunc1 = lambda x, y: 0.05 * y * (1. + 2.e-6 * (x**2 + y**2))

        # It's not required to provide the inverse functions.  However, if we don't, then
        # you will only be able to do toWorld operations, not the inverse toImage.
        # The inverse function does not have to be exact either.  For example, you could provide
        # a function that does some kind of iterative solution to whatever accuracy you care
        # about.  But in this case, we can do the exact inverse.
        #
        # Let w = sqrt(u**2 + v**2) and r = sqrt(x**2 + y**2).  Then the solutions are:
        # x = (u/w) r and y = (u/w) r, and we use Cardano's method to solve for r given w:
        # See http://en.wikipedia.org/wiki/Cubic_function#Cardano.27s_method
        #
        # w = 0.05 r + 2.e-6 * 0.05 * r**3
        # r = 100 * ( ( 5 sqrt(w**2 + 5.e3/27) + 5 w )**(1./3.) -
        #           - ( 5 sqrt(w**2 + 5.e3/27) - 5 w )**(1./3.) )

        def xfunc1(u, v):
            import math
            wsq = u * u + v * v
            if wsq == 0.:
                return 0.
            else:
                w = math.sqrt(wsq)
                temp = 5. * math.sqrt(wsq + 5.e3 / 27)
                r = 100. * ((temp + 5 * w)**(1. / 3.) -
                            (temp - 5 * w)**(1. / 3))
                return u * r / w

        def yfunc1(u, v):
            import math
            wsq = u * u + v * v
            if wsq == 0.:
                return 0.
            else:
                w = math.sqrt(wsq)
                temp = 5. * math.sqrt(wsq + 5.e3 / 27)
                r = 100. * ((temp + 5 * w)**(1. / 3.) -
                            (temp - 5 * w)**(1. / 3))
                return v * r / w

        # You could pass the above functions to UVFunction, and normally we would do that.
        # The only down side to doing so is that the specification of the WCS in the FITS
        # file is rather ugly.  GalSim is able to turn the python byte code into strings,
        # but they are basically a really ugly mess of random-looking characters.  GalSim
        # will be able to read it back in, but human readers will have no idea what WCS
        # function was used.  To see what they look like, uncomment this line and comment
        # out the later wcs line.
        #wcs = galsim.UVFunction(ufunc1, vfunc1, xfunc1, yfunc1, origin=im_center)

        # If you provide the functions as strings, then those strings will be preserved
        # in the FITS header in a form that is more legible to human readers.
        # It also has the extra benefit of matching the output from demo9.yaml, which we
        # always try to do.  The config file has no choice but to specify the functions
        # as strings.

        ufunc = '0.05 * x * (1. + 2.e-6 * (x**2 + y**2))'
        vfunc = '0.05 * y * (1. + 2.e-6 * (x**2 + y**2))'
        xfunc = (
            '( lambda w: ( 0 if w==0 else ' +
            '100.*u/w*(( 5*(w**2 + 5.e3/27.)**0.5 + 5*w )**(1./3.) - ' +
            '( 5*(w**2 + 5.e3/27.)**0.5 - 5*w )**(1./3.))))( (u**2+v**2)**0.5 )'
        )
        yfunc = (
            '( lambda w: ( 0 if w==0 else ' +
            '100.*v/w*(( 5*(w**2 + 5.e3/27.)**0.5 + 5*w )**(1./3.) - ' +
            '( 5*(w**2 + 5.e3/27.)**0.5 - 5*w )**(1./3.))))( (u**2+v**2)**0.5 )'
        )

        # The origin parameter defines where on the image should be considered (x,y) = (0,0)
        # in the WCS functions.
        wcs = galsim.UVFunction(ufunc, vfunc, xfunc, yfunc, origin=im_center)

        # Assign this wcs to full_image
        full_image.wcs = wcs

        # The weight image will hold the inverse variance for each pixel.
        # We can set the wcs directly on construction with the wcs parameter.
        weight_image = galsim.ImageF(image_size, image_size, wcs=wcs)

        # It is common for astrometric images to also have a bad pixel mask.  We don't have any
        # defect simulation currently, so our bad pixel masks are currently all zeros.
        # But someday, we plan to add defect functionality to GalSim, at which point, we'll
        # be able to mark those defects on a bad pixel mask.
        # Note: the S in ImageS means to use "short int" for the data type.
        # This is a typical choice for a bad pixel image.
        badpix_image = galsim.ImageS(image_size, image_size, wcs=wcs)

        # We also draw a PSF image at the location of every galaxy.  This isn't normally done,
        # and since some of the PSFs overlap, it's not necessarily so useful to have this kind
        # of image.  But in this case, it's fun to look at the psf image, especially with
        # something like log scaling in ds9 to see how crazy an aberrated OpticalPSF with
        # struts can look when there is no atmospheric component to blur it out.
        psf_image = galsim.ImageF(image_size, image_size, wcs=wcs)

        # We will also write some truth information to an output catalog.
        # In real simulations, it is often useful to have a catalog of the truth values
        # to compare to measurements either directly or as cuts on the galaxy sample to
        # find where systematic errors are largest.
        # For now, we just make an empty OutputCatalog object with the names and types of the
        # columns.
        names = [
            'object_id', 'halo_id', 'flux', 'radius', 'h_over_r',
            'inclination.rad', 'theta.rad', 'mu', 'redshift', 'shear.g1',
            'shear.g2', 'pos.x', 'pos.y', 'image_pos.x', 'image_pos.y',
            'halo_mass', 'halo_conc', 'halo_redshift'
        ]
        types = [
            int, int, float, float, float, float, float, float, float, float,
            float, float, float, float, float, float, float, float
        ]
        truth_cat = galsim.OutputCatalog(names, types)

        # Setup the NFWHalo stuff:
        nfw = galsim.NFWHalo(mass=mass,
                             conc=nfw_conc,
                             redshift=nfw_z_halo,
                             omega_m=omega_m,
                             omega_lam=omega_lam)
        # Note: the last two are optional.  If they are omitted, then (omega_m=0.3, omega_lam=0.7)
        # are actually the defaults.  If you only specify one of them, the other is set so that
        # the total is 1.  But you can define both values so that the total is not 1 if you want.
        # Radiation is assumed to be zero and dark energy equation of state w = -1.
        # If you want to include either radiation or more complicated dark energy models,
        # you can define your own cosmology class that defines the functions a(z), E(a), and
        # Da(z_source, z_lens).  Then you can pass this to NFWHalo as a `cosmo` parameter.

        # Make the PSF profile outside the loop to minimize the (significant) OpticalPSF
        # construction overhead.
        psf = galsim.OpticalPSF(lam=psf_lam,
                                diam=psf_D,
                                obscuration=psf_obsc,
                                nstruts=psf_nstruts,
                                strut_thick=psf_strut_thick,
                                strut_angle=psf_strut_angle,
                                defocus=psf_defocus,
                                astig1=psf_astig1,
                                astig2=psf_astig2,
                                coma1=psf_coma1,
                                coma2=psf_coma2,
                                trefoil1=psf_trefoil1,
                                trefoil2=psf_trefoil2)

        for k in range(nobj):

            # Initialize the random number generator we will be using for this object:
            ud = galsim.UniformDeviate(seed + k + 1)

            # Determine where this object is going to go.
            # We choose points randomly within a donut centered at the center of the main image
            # in order to avoid placing galaxies too close to the halo center where the lensing
            # is not weak.  We use an inner radius of 3 arcsec and an outer radius of 12 arcsec,
            # which takes us essentially to the edge of the image.
            radius = 12
            inner_radius = 3
            max_rsq = radius**2
            min_rsq = inner_radius**2
            while True:  # (This is essentially a do..while loop.)
                x = (2. * ud() - 1) * radius
                y = (2. * ud() - 1) * radius
                rsq = x**2 + y**2
                if rsq >= min_rsq and rsq <= max_rsq: break
            pos = galsim.PositionD(x, y)

            # We also need the position in pixels to determine where to place the postage
            # stamp on the full image.
            image_pos = wcs.toImage(pos)

            # For even-sized postage stamps, the nominal center (returned by stamp.bounds.center())
            # cannot be at the true center (returned by stamp.bounds.trueCenter()) of the postage
            # stamp, since the nominal center values have to be integers.  Thus, the nominal center
            # is 1/2 pixel up and to the right of the true center.
            # If we used odd-sized postage stamps, we wouldn't need to do this.
            x_nominal = image_pos.x + 0.5
            y_nominal = image_pos.y + 0.5

            # Get the integer values of these which will be the actual nominal center of the
            # postage stamp image.
            ix_nominal = int(math.floor(x_nominal + 0.5))
            iy_nominal = int(math.floor(y_nominal + 0.5))

            # The remainder will be accounted for in an offset when we draw.
            dx = x_nominal - ix_nominal
            dy = y_nominal - iy_nominal
            offset = galsim.PositionD(dx, dy)

            # Draw the flux from a power law distribution: N(f) ~ f^-1.5
            # For this, we use the class DistDeviate which can draw deviates from an arbitrary
            # probability distribution.  This distribution can be defined either as a functional
            # form as we do here, or as tabulated lists of x and p values, from which the
            # function is interpolated.
            flux_dist = galsim.DistDeviate(ud,
                                           function=lambda x: x**-1.5,
                                           x_min=gal_flux_min,
                                           x_max=gal_flux_max)
            flux = flux_dist()

            # We introduce here another surface brightness profile, called InclinedExponential.
            # It represents a typical 3D galaxy disk profile inclined at an arbitrary angle
            # relative to face on.
            #
            #     inclination =  0 degrees corresponds to a face-on disk, which is equivalent to
            #                             the regular Exponential profile.
            #     inclination = 90 degrees corresponds to an edge-on disk.
            #
            # A random orientation corresponds to the inclination angle taking the probability
            # distribution:
            #
            #     P(inc) = 0.5 sin(inc)
            #
            # so we again use a DistDeviate to generate these values.
            inc_dist = galsim.DistDeviate(ud,
                                          function=lambda x: 0.5 * math.sin(x),
                                          x_min=0,
                                          x_max=math.pi)
            inclination = inc_dist() * galsim.radians

            # The parameters scale_radius and scale_height give the scale distances in the
            # 3D distribution:
            #
            #     I(R,z) = I_0 / (2 scale_height) * sech^2(z/scale_height) * exp(-r/scale_radius)
            #
            # These values can be given separately if desired.  However, it is often easier to
            # give the ratio scale_h_over_r as an independent value, since the radius and height
            # values are correlated, while h/r is approximately independent of h or r.
            h_over_r = ud() * (gal_h_over_r_max -
                               gal_h_over_r_min) + gal_h_over_r_min

            radius = ud() * (gal_r_max - gal_r_min) + gal_r_min

            # The inclination is around the x-axis, so we want to rotate the galaxy by a
            # random angle.
            theta = ud() * math.pi * 2. * galsim.radians

            # Make the galaxy profile with these values:
            gal = galsim.InclinedExponential(scale_radius=radius,
                                             scale_h_over_r=h_over_r,
                                             inclination=inclination,
                                             flux=flux)
            gal = gal.rotate(theta)

            # Now apply the appropriate lensing effects for this position from
            # the NFW halo mass.
            try:
                g1, g2 = nfw.getShear(pos, nfw_z_source)
                nfw_shear = galsim.Shear(g1=g1, g2=g2)
            except:
                # This shouldn't happen, since we exclude the inner 10 arcsec, but it's a
                # good idea to use the try/except block here anyway.
                import warnings
                warnings.warn(
                    "Warning: NFWHalo shear is invalid -- probably strong lensing!  "
                    + "Using shear = 0.")
                nfw_shear = galsim.Shear(g1=0, g2=0)

            nfw_mu = nfw.getMagnification(pos, nfw_z_source)
            if nfw_mu < 0:
                import warnings
                warnings.warn(
                    "Warning: mu < 0 means strong lensing!  Using mu=25.")
                nfw_mu = 25
            elif nfw_mu > 25:
                import warnings
                warnings.warn(
                    "Warning: mu > 25 means strong lensing!  Using mu=25.")
                nfw_mu = 25

            # Calculate the total shear to apply
            # Since shear addition is not commutative, it is worth pointing out that
            # the order is in the sense that the second shear is applied first, and then
            # the first shear.  i.e. The field shear is taken to be behind the cluster.
            # Kind of a cosmic shear contribution between the source and the cluster.
            # However, this is not quite the same thing as doing:
            #     gal.shear(field_shear).shear(nfw_shear)
            # since the shear addition ignores the rotation that would occur when doing the
            # above lines.  This is normally ok, because the rotation is not observable, but
            # it is worth keeping in mind.
            total_shear = nfw_shear + field_shear

            # Apply the magnification and shear to the galaxy
            gal = gal.magnify(nfw_mu)
            gal = gal.shear(total_shear)

            # Build the final object
            final = galsim.Convolve([psf, gal])

            # Draw the stamp image
            # To draw the image at a position other than the center of the image, you can
            # use the offset parameter, which applies an offset in pixels relative to the
            # center of the image.
            # We also need to provide the local wcs at the current position.
            local_wcs = wcs.local(image_pos)
            stamp = final.drawImage(wcs=local_wcs, offset=offset)

            # Recenter the stamp at the desired position:
            stamp.setCenter(ix_nominal, iy_nominal)

            # Find overlapping bounds
            bounds = stamp.bounds & full_image.bounds
            full_image[bounds] += stamp[bounds]

            # Also draw the PSF
            psf_stamp = galsim.ImageF(
                stamp.bounds)  # Use same bounds as galaxy stamp
            psf.drawImage(psf_stamp, wcs=local_wcs, offset=offset)
            psf_image[bounds] += psf_stamp[bounds]

            # Add the truth information for this object to the truth catalog
            row = ((first_obj_id + k), halo_id, flux, radius, h_over_r,
                   inclination.rad(), theta.rad(), nfw_mu, nfw_z_source,
                   total_shear.g1, total_shear.g2, pos.x, pos.y, image_pos.x,
                   image_pos.y, mass, nfw_conc, nfw_z_halo)
            truth_cat.addRow(row)

        # Add Poisson noise to the full image
        # Note: The normal calculation of Poission noise isn't quite correct right now.
        # The pixel area is variable, which means the amount of sky flux that enters each
        # pixel is also variable.  The wcs classes have a function `makeSkyImage` which
        # will fill an image with the correct amount of sky flux given the sky level
        # in units of ADU/arcsec^2.  We use the weight image as our work space for this.
        wcs.makeSkyImage(weight_image, sky_level)

        # Add this to the current full_image (temporarily).
        full_image += weight_image

        # Add Poisson noise, given the current full_image.
        # The config parser uses a different random number generator for file-level and
        # image-level values than for the individual objects.  This makes it easier to
        # parallelize the calculation if desired.  In fact, this is why we've been adding 1
        # to each seed value all along.  The seeds for the objects take the values
        # random_seed+1 .. random_seed+nobj.  The seed for the image is just random_seed,
        # which we built already (below) when we calculated how many objects need to
        # be in each file.  Use the same rng again here, since this is also at image scope.
        full_image.addNoise(galsim.PoissonNoise(rng))

        # Subtract the sky back off.
        full_image -= weight_image

        # The weight image is nominally the inverse variance of the pixel noise.  However, it is
        # common to exclude the Poisson noise from the objects themselves and only include the
        # noise from the sky photons.  The variance of the noise is just the sky level, which is
        # what is currently in the weight_image.  (If we wanted to include the variance from the
        # objects too, then we could use the full_image before we added the PoissonNoise to it.)
        # So all we need to do now is to invert the values in weight_image.
        weight_image.invertSelf()

        # Write the file to disk:
        galsim.fits.writeMulti(
            [full_image, badpix_image, weight_image, psf_image], file_name)

        # And write the truth catalog file
        truth_cat.write(truth_file_name)

        t2 = time.time()
        return t2 - t1
Ejemplo n.º 2
0
def test_poisson():
    """Test the Poisson noise builder
    """
    scale = 0.3
    sky = 200

    config = {
        'image' : {
            'type' : 'Single',
            'random_seed' : 1234,
            'pixel_scale' : scale,
            'size' : 32,

            'noise' : {
                'type' : 'Poisson',
                'sky_level' : sky,
            }
        },
        'gal' : {
            'type' : 'Gaussian',
            'sigma' : 1.1,
            'flux' : 100,
        },
    }

    # First build by hand
    rng = galsim.BaseDeviate(1234 + 1)
    gal = galsim.Gaussian(sigma=1.1, flux=100)
    im1a = gal.drawImage(nx=32, ny=32, scale=scale)
    sky_pixel = sky * scale**2
    im1a.addNoise(galsim.PoissonNoise(rng, sky_level=sky_pixel))

    # Compare to what config builds
    im1b = galsim.config.BuildImage(config)
    np.testing.assert_equal(im1b.array, im1a.array)

    # Check noise variance
    var1 = galsim.config.CalculateNoiseVariance(config)
    np.testing.assert_equal(var1, sky_pixel)
    var2 = galsim.Image(3,3)
    galsim.config.AddNoiseVariance(config, var2)
    np.testing.assert_almost_equal(var2.array, sky_pixel)

    # Check include_obj_var=True
    var3 = galsim.Image(32,32)
    galsim.config.AddNoiseVariance(config, var3, include_obj_var=True)
    np.testing.assert_almost_equal(var3.array, sky_pixel + im1a.array)

    # Repeat using photon shooting, which needs to do something slightly different, since the
    # signal photons already have shot noise.
    rng.seed(1234 + 1)
    im2a = gal.drawImage(nx=32, ny=32, scale=scale, method='phot', rng=rng)
    # Need to add Poisson noise for the sky, but not the signal (which already has shot noise)
    im2a.addNoise(galsim.DeviateNoise(galsim.PoissonDeviate(rng, mean=sky_pixel)))
    im2a -= sky_pixel

    # Compare to what config builds
    galsim.config.RemoveCurrent(config)
    config['image']['draw_method'] = 'phot'  # Make sure it gets copied over to stamp properly.
    del config['stamp']['draw_method']
    del config['_copied_image_keys_to_stamp']
    im2b = galsim.config.BuildImage(config)
    np.testing.assert_equal(im2b.array, im2a.array)

    # Check non-trivial sky image
    galsim.config.RemoveCurrent(config)
    config['image']['sky_level'] = sky
    config['image']['wcs'] =  {
        'type' : 'UVFunction',
        'ufunc' : '0.05*x + 0.001*x**2',
        'vfunc' : '0.05*y + 0.001*y**2',
    }
    del config['image']['pixel_scale']
    del config['wcs']
    rng.seed(1234+1)
    wcs = galsim.UVFunction(ufunc='0.05*x + 0.001*x**2', vfunc='0.05*y + 0.001*y**2')
    im3a = gal.drawImage(nx=32, ny=32, wcs=wcs, method='phot', rng=rng)
    sky_im = galsim.Image(im3a.bounds, wcs=wcs)
    wcs.makeSkyImage(sky_im, sky)
    im3a += sky_im  # Add 1 copy of the raw sky image for image[sky]
    noise_im = sky_im.copy()
    noise_im *= 2.  # Now 2x because the noise includes both in image[sky] and noise[sky]
    noise_im.addNoise(galsim.PoissonNoise(rng))
    noise_im -= 2.*sky_im
    im3a += noise_im
    im3b = galsim.config.BuildImage(config)
    np.testing.assert_almost_equal(im3b.array, im3a.array, decimal=6)
Ejemplo n.º 3
0
def test_whiten():
    """Test the options in config to whiten images
    """
    real_gal_dir = os.path.join('..','examples','data')
    real_gal_cat = 'real_galaxy_catalog_23.5_example.fits'
    config = {
        'image' : {
            'type' : 'Single',
            'random_seed' : 1234,
            'pixel_scale' : 0.05,
        },
        'stamp' : {
            'type' : 'Basic',
            'size' : 32,
        },
        'gal' : {
            'type' : 'RealGalaxy',
            'index' : 79,
            'flux' : 100,
        },
        'psf' : {  # This is really slow if we don't convolve by a PSF.
            'type' : 'Gaussian',
            'sigma' : 0.05
        },
        'input' : {
            'real_catalog' : {
                'dir' : real_gal_dir ,
                'file_name' : real_gal_cat
            }
        }
    }

    # First build by hand (no whitening yet)
    rng = galsim.BaseDeviate(1234 + 1)
    rgc = galsim.RealGalaxyCatalog(os.path.join(real_gal_dir, real_gal_cat))
    gal = galsim.RealGalaxy(rgc, index=79, flux=100, rng=rng)
    psf = galsim.Gaussian(sigma=0.05)
    final = galsim.Convolve(gal,psf)
    im1a = final.drawImage(nx=32, ny=32, scale=0.05)

    # Compare to what config builds
    galsim.config.ProcessInput(config)
    im1b, cv1b = galsim.config.BuildStamp(config, do_noise=False)
    np.testing.assert_equal(cv1b, 0.)
    np.testing.assert_equal(im1b.array, im1a.array)

    # Now add whitening, but no noise yet.
    cv1a = final.noise.whitenImage(im1a)
    print('From whiten, current_var = ',cv1a)
    galsim.config.RemoveCurrent(config)
    config['image']['noise'] =  { 'whiten' : True, }
    im1c, cv1c = galsim.config.BuildStamp(config, do_noise=False)
    print('From BuildStamp, current_var = ',cv1c)
    np.testing.assert_equal(cv1c, cv1a)
    np.testing.assert_equal(im1c.array, im1a.array)
    rng1 = rng.duplicate()  # Save current state of rng

    # 1. Gaussian noise
    #####
    config['image']['noise'] =  {
        'type' : 'Gaussian',
        'variance' : 50,
        'whiten' : True,
    }
    galsim.config.RemoveCurrent(config)
    im2a = im1a.copy()
    im2a.addNoise(galsim.GaussianNoise(sigma=math.sqrt(50-cv1a), rng=rng))
    im2b, cv2b = galsim.config.BuildStamp(config)
    np.testing.assert_almost_equal(cv2b, 50)
    np.testing.assert_almost_equal(im2b.array, im2a.array, decimal=5)

    # If whitening already added too much noise, raise an exception
    config['image']['noise']['variance'] = 1.e-5
    try:
        np.testing.assert_raises(RuntimeError, galsim.config.BuildStamp,config)
    except ImportError:
        pass

    # 2. Poisson noise
    #####
    config['image']['noise'] =  {
        'type' : 'Poisson',
        'sky_level_pixel' : 50,
        'whiten' : True,
    }
    galsim.config.RemoveCurrent(config)
    im3a = im1a.copy()
    sky = 50 - cv1a
    rng.reset(rng1.duplicate())
    im3a.addNoise(galsim.PoissonNoise(sky_level=sky, rng=rng))
    im3b, cv3b = galsim.config.BuildStamp(config)
    np.testing.assert_almost_equal(cv3b, 50, decimal=5)
    np.testing.assert_almost_equal(im3b.array, im3a.array, decimal=5)

    # It's more complicated if the sky is quoted per arcsec and the wcs is not uniform.
    config2 = galsim.config.CopyConfig(config)
    galsim.config.RemoveCurrent(config2)
    config2['image']['sky_level'] = 100
    config2['image']['wcs'] =  {
        'type' : 'UVFunction',
        'ufunc' : '0.05*x + 0.001*x**2',
        'vfunc' : '0.05*y + 0.001*y**2',
    }
    del config2['image']['pixel_scale']
    del config2['wcs']
    config2['image']['noise']['symmetrize'] = 4 # Also switch to symmetrize, just to mix it up.
    del config2['image']['noise']['whiten']
    rng.reset(1234+1) # Start fresh, since redoing the whitening/symmetrizing
    wcs = galsim.UVFunction(ufunc='0.05*x + 0.001*x**2', vfunc='0.05*y + 0.001*y**2')
    im3c = galsim.Image(32,32, wcs=wcs)
    im3c = final.drawImage(im3c)
    cv3c = final.noise.symmetrizeImage(im3c,4)
    sky = galsim.Image(im3c.bounds, wcs=wcs)
    wcs.makeSkyImage(sky, 100)
    mean_sky = np.mean(sky.array)
    im3c += sky
    extra_sky = 50 - cv3c
    im3c.addNoise(galsim.PoissonNoise(sky_level=extra_sky, rng=rng))
    im3d, cv3d = galsim.config.BuildStamp(config2)
    np.testing.assert_almost_equal(cv3d, 50 + mean_sky, decimal=4)
    np.testing.assert_almost_equal(im3d.array, im3c.array, decimal=5)

    config['image']['noise']['sky_level_pixel'] = 1.e-5
    try:
        np.testing.assert_raises(RuntimeError, galsim.config.BuildStamp,config)
    except ImportError:
        pass

    # 3. CCDNoise
    #####
    config['image']['noise'] =  {
        'type' : 'CCD',
        'sky_level_pixel' : 25,
        'read_noise' : 5,
        'gain' : 1,
        'whiten' : True,
    }
    galsim.config.RemoveCurrent(config)
    im4a = im1a.copy()
    rn = math.sqrt(25-cv1a)
    rng.reset(rng1.duplicate())
    im4a.addNoise(galsim.CCDNoise(sky_level=25, read_noise=rn, gain=1, rng=rng))
    im4b, cv4b = galsim.config.BuildStamp(config)
    np.testing.assert_almost_equal(cv4b, 50, decimal=5)
    np.testing.assert_almost_equal(im4b.array, im4a.array, decimal=5)

    # Repeat with gain != 1
    config['image']['noise']['gain'] = 3.7
    galsim.config.RemoveCurrent(config)
    im5a = im1a.copy()
    rn = math.sqrt(25-cv1a * 3.7**2)
    rng.reset(rng1.duplicate())
    im5a.addNoise(galsim.CCDNoise(sky_level=25, read_noise=rn, gain=3.7, rng=rng))
    im5b, cv5b = galsim.config.BuildStamp(config)
    np.testing.assert_almost_equal(cv5b, 50, decimal=5)
    np.testing.assert_almost_equal(im5b.array, im5a.array, decimal=5)

    # And again with a non-trivial sky image
    galsim.config.RemoveCurrent(config2)
    config2['image']['noise'] = config['image']['noise']
    config2['image']['noise']['symmetrize'] = 4
    del config2['image']['noise']['whiten']
    rng.reset(1234+1)
    im5c = galsim.Image(32,32, wcs=wcs)
    im5c = final.drawImage(im5c)
    cv5c = final.noise.symmetrizeImage(im5c, 4)
    sky = galsim.Image(im5c.bounds, wcs=wcs)
    wcs.makeSkyImage(sky, 100)
    mean_sky = np.mean(sky.array)
    im5c += sky
    rn = math.sqrt(25-cv5c * 3.7**2)
    im5c.addNoise(galsim.CCDNoise(sky_level=25, read_noise=rn, gain=3.7, rng=rng))
    im5d, cv5d = galsim.config.BuildStamp(config2)
    np.testing.assert_almost_equal(cv5d, 50 + mean_sky, decimal=4)
    np.testing.assert_almost_equal(im5d.array, im5c.array, decimal=5)

    config['image']['noise']['sky_level_pixel'] = 1.e-5
    config['image']['noise']['read_noise'] = 0
    try:
        np.testing.assert_raises(RuntimeError, galsim.config.BuildStamp,config)
    except ImportError:
        pass

    # 4. COSMOSNoise
    #####
    file_name = os.path.join(galsim.meta_data.share_dir,'acs_I_unrot_sci_20_cf.fits')
    config['image']['noise'] =  {
        'type' : 'COSMOS',
        'file_name' : file_name,
        'variance' : 50,
        'whiten' : True,
    }
    galsim.config.RemoveCurrent(config)
    im6a = im1a.copy()
    rng.reset(rng1.duplicate())
    noise = galsim.getCOSMOSNoise(file_name=file_name, variance=50, rng=rng)
    noise -= galsim.UncorrelatedNoise(cv1a, rng=rng, wcs=noise.wcs)
    im6a.addNoise(noise)
    im6b, cv6b = galsim.config.BuildStamp(config)
    np.testing.assert_almost_equal(cv6b, 50, decimal=5)
    np.testing.assert_almost_equal(im6b.array, im6a.array, decimal=5)

    config['image']['noise']['variance'] = 1.e-5
    del config['_current_cn_tag']
    try:
        np.testing.assert_raises(RuntimeError, galsim.config.BuildStamp,config)
    except ImportError:
        pass
Ejemplo n.º 4
0
def test_ccdnoise_phot():
    """CCDNoise has some special code for photon shooting, so check that it works correctly.
    """
    scale = 0.3
    sky = 200
    gain = 1.8
    rn = 2.3

    config = {
        'image' : {
            'type' : 'Single',
            'random_seed' : 1234,
            'pixel_scale' : scale,
            'size' : 32,
            'draw_method' : 'phot',

            'noise' : {
                'type' : 'CCD',
                'gain' : gain,
                'read_noise' : rn,
                'sky_level' : sky,
            }
        },
        'gal' : {
            'type' : 'Gaussian',
            'sigma' : 1.1,
            'flux' : 100,
        },
    }

    # First build by hand
    rng = galsim.BaseDeviate(1234 + 1)
    gal = galsim.Gaussian(sigma=1.1, flux=100)
    im1a = gal.drawImage(nx=32, ny=32, scale=scale, method='phot', rng=rng)
    sky_pixel = sky * scale**2
    # Need to add Poisson noise for the sky, but not the signal (which already has shot noise)
    im1a *= gain
    im1a.addNoise(galsim.DeviateNoise(galsim.PoissonDeviate(rng, mean=sky_pixel * gain)))
    im1a /= gain
    im1a -= sky_pixel
    im1a.addNoise(galsim.GaussianNoise(rng, sigma=rn/gain))

    # Compare to what config builds
    im1b = galsim.config.BuildImage(config)
    np.testing.assert_equal(im1b.array, im1a.array)

    # Check noise variance
    var = sky_pixel / gain + rn**2 / gain**2
    var1 = galsim.config.CalculateNoiseVariance(config)
    np.testing.assert_equal(var1, var)
    var2 = galsim.Image(3,3)
    galsim.config.AddNoiseVariance(config, var2)
    np.testing.assert_almost_equal(var2.array, var)

    # Check include_obj_var=True
    var3 = galsim.Image(32,32)
    galsim.config.AddNoiseVariance(config, var3, include_obj_var=True)
    np.testing.assert_almost_equal(var3.array, var + im1a.array/gain)

    # Some slightly different code paths if rn = 0 or gain = 1:
    del config['image']['noise']['gain']
    del config['image']['noise']['read_noise']
    del config['image']['noise']['_get']
    rng.seed(1234 + 1)
    im2a = gal.drawImage(nx=32, ny=32, scale=scale, method='phot', rng=rng)
    im2a.addNoise(galsim.DeviateNoise(galsim.PoissonDeviate(rng, mean=sky_pixel)))
    im2a -= sky_pixel
    im2b = galsim.config.BuildImage(config)
    np.testing.assert_equal(im2b.array, im2a.array)
    var5 = galsim.config.CalculateNoiseVariance(config)
    np.testing.assert_equal(var5, sky_pixel)
    var6 = galsim.Image(3,3)
    galsim.config.AddNoiseVariance(config, var6)
    np.testing.assert_almost_equal(var6.array, sky_pixel)
    var7 = galsim.Image(32,32)
    galsim.config.AddNoiseVariance(config, var7, include_obj_var=True)
    np.testing.assert_almost_equal(var7.array, sky_pixel + im2a.array)

    # Check non-trivial sky image
    galsim.config.RemoveCurrent(config)
    config['image']['sky_level'] = sky
    config['image']['wcs'] =  {
        'type' : 'UVFunction',
        'ufunc' : '0.05*x + 0.001*x**2',
        'vfunc' : '0.05*y + 0.001*y**2',
    }
    del config['image']['pixel_scale']
    del config['wcs']
    rng.seed(1234+1)
    wcs = galsim.UVFunction(ufunc='0.05*x + 0.001*x**2', vfunc='0.05*y + 0.001*y**2')
    im3a = gal.drawImage(nx=32, ny=32, wcs=wcs, method='phot', rng=rng)
    sky_im = galsim.Image(im3a.bounds, wcs=wcs)
    wcs.makeSkyImage(sky_im, sky)
    im3a += sky_im  # Add 1 copy of the raw sky image for image[sky]
    noise_im = sky_im.copy()
    noise_im *= 2.  # Now 2x because the noise includes both in image[sky] and noise[sky]
    noise_im.addNoise(galsim.PoissonNoise(rng))
    noise_im -= 2.*sky_im
    im3a += noise_im
    im3b = galsim.config.BuildImage(config)
    np.testing.assert_almost_equal(im3b.array, im3a.array, decimal=6)

    # And again with the rn and gain put back in.
    galsim.config.RemoveCurrent(config)
    config['image']['noise']['gain'] = gain
    config['image']['noise']['read_noise'] = rn
    del config['image']['noise']['_get']
    rng.seed(1234+1)
    im4a = gal.drawImage(nx=32, ny=32, wcs=wcs, method='phot', rng=rng)
    wcs.makeSkyImage(sky_im, sky)
    im4a += sky_im
    noise_im = sky_im.copy()
    noise_im *= 2. * gain
    noise_im.addNoise(galsim.PoissonNoise(rng))
    noise_im /= gain
    noise_im -= 2. * sky_im
    im4a += noise_im
    im4a.addNoise(galsim.GaussianNoise(rng, sigma=rn/gain))
    im4b = galsim.config.BuildImage(config)
    np.testing.assert_almost_equal(im4b.array, im4a.array, decimal=6)
Ejemplo n.º 5
0
def test_withOrigin():
    from test_wcs import Cubic

    # First EuclideantWCS types:

    wcs_list = [
        galsim.OffsetWCS(0.3, galsim.PositionD(1, 1), galsim.PositionD(10,
                                                                       23)),
        galsim.OffsetShearWCS(0.23, galsim.Shear(g1=0.1, g2=0.3),
                              galsim.PositionD(12, 43)),
        galsim.AffineTransform(0.01, 0.26, -0.26, 0.02,
                               galsim.PositionD(12, 43)),
        galsim.UVFunction(ufunc=lambda x, y: 0.2 * x,
                          vfunc=lambda x, y: 0.2 * y),
        galsim.UVFunction(ufunc=lambda x, y: 0.2 * x,
                          vfunc=lambda x, y: 0.2 * y,
                          xfunc=lambda u, v: u / scale,
                          yfunc=lambda u, v: v / scale),
        galsim.UVFunction(ufunc='0.2*x + 0.03*y', vfunc='0.01*x + 0.2*y'),
    ]

    color = 0.3
    for wcs in wcs_list:
        # Original version of the shiftOrigin tests in do_nonlocal_wcs using deprecated name.
        new_origin = galsim.PositionI(123, 321)
        wcs3 = check_dep(wcs.withOrigin, new_origin)
        assert wcs != wcs3, name + ' is not != wcs.withOrigin(pos)'
        wcs4 = wcs.local(wcs.origin, color=color)
        assert wcs != wcs4, name + ' is not != wcs.local()'
        assert wcs4 != wcs, name + ' is not != wcs.local() (reverse)'
        world_origin = wcs.toWorld(wcs.origin, color=color)
        if wcs.isUniform():
            if wcs.world_origin == galsim.PositionD(0, 0):
                wcs2 = wcs.local(wcs.origin,
                                 color=color).withOrigin(wcs.origin)
                assert wcs == wcs2, name + ' is not equal after wcs.local().withOrigin(origin)'
            wcs2 = wcs.local(wcs.origin,
                             color=color).withOrigin(wcs.origin,
                                                     wcs.world_origin)
            assert wcs == wcs2, name + ' not equal after wcs.local().withOrigin(origin,world_origin)'
        world_pos1 = wcs.toWorld(galsim.PositionD(0, 0), color=color)
        wcs3 = check_dep(wcs.withOrigin, new_origin)
        world_pos2 = wcs3.toWorld(new_origin, color=color)
        np.testing.assert_almost_equal(
            world_pos2.x, world_pos1.x, 7,
            'withOrigin(new_origin) returned wrong world position')
        np.testing.assert_almost_equal(
            world_pos2.y, world_pos1.y, 7,
            'withOrigin(new_origin) returned wrong world position')
        new_world_origin = galsim.PositionD(5352.7, 9234.3)
        wcs5 = check_dep(wcs.withOrigin,
                         new_origin,
                         new_world_origin,
                         color=color)
        world_pos3 = wcs5.toWorld(new_origin, color=color)
        np.testing.assert_almost_equal(
            world_pos3.x, new_world_origin.x, 7,
            'withOrigin(new_origin, new_world_origin) returned wrong position')
        np.testing.assert_almost_equal(
            world_pos3.y, new_world_origin.y, 7,
            'withOrigin(new_origin, new_world_origin) returned wrong position')

    # Now some CelestialWCS types
    cubic_u = Cubic(2.9e-5, 2000., 'u')
    cubic_v = Cubic(-3.7e-5, 2000., 'v')
    center = galsim.CelestialCoord(23 * galsim.degrees, -13 * galsim.degrees)
    radec = lambda x, y: center.deproject_rad(
        cubic_u(x, y) * 0.2, cubic_v(x, y) * 0.2, projection='lambert')
    wcs_list = [
        galsim.RaDecFunction(radec),
        galsim.AstropyWCS('1904-66_TAN.fits', dir='fits_files'),
        galsim.GSFitsWCS('tpv.fits', dir='fits_files'),
        galsim.FitsWCS('sipsample.fits', dir='fits_files'),
    ]

    for wcs in wcs_list:
        # Original version of the shiftOrigin tests in do_celestial_wcs using deprecated name.
        new_origin = galsim.PositionI(123, 321)
        wcs3 = wcs.shiftOrigin(new_origin)
        assert wcs != wcs3, name + ' is not != wcs.shiftOrigin(pos)'
        wcs4 = wcs.local(wcs.origin)
        assert wcs != wcs4, name + ' is not != wcs.local()'
        assert wcs4 != wcs, name + ' is not != wcs.local() (reverse)'
        world_pos1 = wcs.toWorld(galsim.PositionD(0, 0))
        wcs3 = wcs.shiftOrigin(new_origin)
        world_pos2 = wcs3.toWorld(new_origin)
        np.testing.assert_almost_equal(
            world_pos2.distanceTo(world_pos1) / galsim.arcsec, 0, 7,
            'shiftOrigin(new_origin) returned wrong world position')
Ejemplo n.º 6
0
def test_poisson():
    """Test the Poisson noise builder
    """
    scale = 0.3
    sky = 200

    config = {
        'image' : {
            'type' : 'Single',
            'random_seed' : 1234,
            'pixel_scale' : scale,
            'size' : 32,

            'noise' : {
                'type' : 'Poisson',
                'sky_level' : sky,
            }
        },
        'gal' : {
            'type' : 'Gaussian',
            'sigma' : 1.1,
            'flux' : 100,
        },
    }

    # First build by hand
    rng = galsim.BaseDeviate(1234 + 1)
    gal = galsim.Gaussian(sigma=1.1, flux=100)
    im1a = gal.drawImage(nx=32, ny=32, scale=scale)
    sky_pixel = sky * scale**2
    im1a.addNoise(galsim.PoissonNoise(rng, sky_level=sky_pixel))

    # Compare to what config builds
    im1b = galsim.config.BuildImage(config)
    np.testing.assert_equal(im1b.array, im1a.array)

    # Check noise variance
    var1 = galsim.config.CalculateNoiseVariance(config)
    np.testing.assert_equal(var1, sky_pixel)
    var2 = galsim.Image(3,3)
    galsim.config.AddNoiseVariance(config, var2)
    np.testing.assert_almost_equal(var2.array, sky_pixel)

    # Check include_obj_var=True
    var3 = galsim.Image(32,32)
    galsim.config.AddNoiseVariance(config, var3, include_obj_var=True)
    np.testing.assert_almost_equal(var3.array, sky_pixel + im1a.array)

    # Repeat using photon shooting, which needs to do something slightly different, since the
    # signal photons already have shot noise.
    rng.seed(1234 + 1)
    im2a = gal.drawImage(nx=32, ny=32, scale=scale, method='phot', rng=rng)
    # Need to add Poisson noise for the sky, but not the signal (which already has shot noise)
    im2a.addNoise(galsim.DeviateNoise(galsim.PoissonDeviate(rng, mean=sky_pixel)))
    im2a -= sky_pixel

    # Compare to what config builds
    galsim.config.RemoveCurrent(config)
    config['image']['draw_method'] = 'phot'  # Make sure it gets copied over to stamp properly.
    del config['stamp']['draw_method']
    del config['stamp']['_done']
    im2b = galsim.config.BuildImage(config)
    np.testing.assert_equal(im2b.array, im2a.array)

    # Check non-trivial sky image
    galsim.config.RemoveCurrent(config)
    config['image']['sky_level'] = sky
    config['image']['wcs'] =  {
        'type' : 'UVFunction',
        'ufunc' : '0.05*x + 0.001*x**2',
        'vfunc' : '0.05*y + 0.001*y**2',
    }
    del config['image']['pixel_scale']
    del config['wcs']
    rng.seed(1234+1)
    wcs = galsim.UVFunction(ufunc='0.05*x + 0.001*x**2', vfunc='0.05*y + 0.001*y**2')
    im3a = gal.drawImage(nx=32, ny=32, wcs=wcs, method='phot', rng=rng)
    sky_im = galsim.Image(im3a.bounds, wcs=wcs)
    wcs.makeSkyImage(sky_im, sky)
    im3a += sky_im  # Add 1 copy of the raw sky image for image[sky]
    noise_im = sky_im.copy()
    noise_im *= 2.  # Now 2x because the noise includes both in image[sky] and noise[sky]
    noise_im.addNoise(galsim.PoissonNoise(rng))
    noise_im -= 2.*sky_im
    im3a += noise_im
    im3b = galsim.config.BuildImage(config)
    np.testing.assert_almost_equal(im3b.array, im3a.array, decimal=6)

    # With tree rings, the sky includes them as well.
    config['image']['sensor'] = {
        'type' : 'Silicon',
        'treering_func' : {
            'type' : 'File',
            'file_name' : 'tree_ring_lookup.dat',
            'amplitude' : 0.5
        },
        'treering_center' : {
            'type' : 'XY',
            'x' : 0,
            'y' : -500
        }
    }
    galsim.config.RemoveCurrent(config)
    config = galsim.config.CleanConfig(config)
    rng.seed(1234+1)
    trfunc = galsim.LookupTable.from_file('tree_ring_lookup.dat', amplitude=0.5)
    sensor = galsim.SiliconSensor(treering_func=trfunc, treering_center=galsim.PositionD(0,-500),
                                  rng=rng)
    im4a = gal.drawImage(nx=32, ny=32, wcs=wcs, method='phot', rng=rng, sensor=sensor)
    sky_im = galsim.Image(im3a.bounds, wcs=wcs)
    wcs.makeSkyImage(sky_im, sky)
    areas = sensor.calculate_pixel_areas(sky_im, use_flux=False)
    sky_im *= areas
    im4a += sky_im
    noise_im = sky_im.copy()
    noise_im *= 2.
    noise_im.addNoise(galsim.PoissonNoise(rng))
    noise_im -= 2.*sky_im
    im4a += noise_im
    im4b = galsim.config.BuildImage(config)
    np.testing.assert_almost_equal(im4b.array, im4a.array, decimal=6)

    # Can't have both sky_level and sky_level_pixel
    config['image']['noise']['sky_level_pixel'] = 2000.
    with assert_raises(galsim.GalSimConfigError):
        galsim.config.BuildImage(config)

    # Must have a valid noise type
    del config['image']['noise']['sky_level_pixel']
    config['image']['noise']['type'] = 'Invalid'
    with assert_raises(galsim.GalSimConfigError):
        galsim.config.BuildImage(config)

    # noise must be a dict
    config['image']['noise'] = 'Invalid'
    with assert_raises(galsim.GalSimConfigError):
        galsim.config.BuildImage(config)

    # Can't have signal_to_noise and  flux
    config['image']['noise'] = { 'type' : 'Poisson', 'sky_level' : sky }
    config['gal']['signal_to_noise'] = 100
    with assert_raises(galsim.GalSimConfigError):
        galsim.config.BuildImage(config)

    # This should work
    del config['gal']['flux']
    galsim.config.BuildImage(config)

    # These now hit the errors in CalculateNoiseVariance rather than AddNoise
    config['image']['noise']['type'] = 'Invalid'
    with assert_raises(galsim.GalSimConfigError):
        galsim.config.BuildImage(config)
    config['image']['noise'] = 'Invalid'
    with assert_raises(galsim.GalSimConfigError):
        galsim.config.BuildImage(config)
    del config['image']['noise']
    with assert_raises(galsim.GalSimConfigError):
        galsim.config.BuildImage(config)

    # If rather than signal_to_noise, we have an extra_weight output, then it hits
    # a different error.
    config['gal']['flux'] = 100
    del config['gal']['signal_to_noise']
    config['output'] = { 'weight' : {} }
    config['image']['noise'] = { 'type' : 'Poisson', 'sky_level' : sky }
    galsim.config.SetupExtraOutput(config)
    galsim.config.SetupConfigFileNum(config, 0, 0, 0)
    # This should work again.
    galsim.config.BuildImage(config)
    config['image']['noise']['type'] = 'Invalid'
    with assert_raises(galsim.GalSimConfigError):
        galsim.config.BuildImage(config)
    config['image']['noise'] = 'Invalid'
    with assert_raises(galsim.GalSimConfigError):
        galsim.config.BuildImage(config)