예제 #1
0
    def __init__(self, file_name, image_file_name=None, wcs=None, dir=None):

        if dir:
            import os
            file_name = os.path.join(dir, file_name)
            image_file_name = os.path.join(dir, image_file_name)
        self.file_name = file_name
        if image_file_name:
            if wcs is not None:
                raise AttributeError(
                    "Cannot provide both image_file_name and wcs")
            self.wcs = galsim.GSFitsWCS(image_file_name)
        elif wcs:
            self.wcs = wcs
        else:
            self.wcs = None
        self.read()
예제 #2
0
파일: test_des.py 프로젝트: mwvgroup/GalSim
def test_nan_fits():
    """Test reading in a FITS file that has NAN.0 entries in the header.

    This test is specifically in response to issue #602.
    """
    import warnings
    from galsim._pyfits import pyfits
    # Older pyfits versions don't have this, so just skip this test then.
    if not hasattr(pyfits, 'verify'): return

    # The problematic file:
    file_name = "des_data/DECam_00158414_01.fits.fz"

    # These are the values we should be reading in:
    ref_bounds = galsim.BoundsI(xmin=1, xmax=2048, ymin=1, ymax=4096)
    ref_wcs = galsim.GSFitsWCS(_data=[
        'TPV',
        numpy.array([13423.2, 6307.333]),
        numpy.array([[-4.410051713005e-09, 7.286844513153e-05],
                     [-7.285161461796e-05, 3.936353853081e-09]]),
        galsim.CelestialCoord(1.1502513773465992 *
                              galsim.radians, -0.9862866578241959 *
                              galsim.radians),
        numpy.array([[[
            0.004336243600183, -0.01133740904139, 0.01202041999278,
            -0.004357212119479
        ], [1.013741474567, -0.01657049389296, 0.005805882078771, 0.0],
                      [0.008865811106037, -0.007472254968395, 0.0, 0.0],
                      [0.0008534196190617, 0.0, 0.0, 0.0]],
                     [[
                         0.002619866608142, 0.9931356822158, 0.008771460618847,
                         -0.003739430249945
                     ],
                      [
                          -0.009422336649176, 0.01826140592329,
                          -0.009387805146152, 0.0
                      ], [-0.01066967054507, 0.007202907073747, 0.0, 0.0],
                      [-0.003683686751425, 0.0, 0.0, 0.0]]]), None, None
    ])

    # First just read the file directly, not using galsim.fits.read
    with pyfits.open(file_name) as fp:
        try:
            data = fp[1].data
            print('Able to read FITS file with NAN.0 without any problem.')
        except:
            print('Running verify to fix the problematic FITS header.')
            with warnings.catch_warnings():
                warnings.filterwarnings("ignore",
                                        category=pyfits.verify.VerifyWarning)
                fp[1].verify('fix')
            # This should work now.
            data = fp[1].data
        header = fp[1].header

    assert data.shape == ref_bounds.numpyShape()

    # Check a direct read of the header with GSFitsWCS
    wcs = galsim.GSFitsWCS(header=header)
    assert wcs == ref_wcs

    # Now read it with GalSim's fits.read function.
    # Reading this file will emit verification warnings, so we'll ignore those here for the
    # test.  But the result should be a valid image.
    with warnings.catch_warnings():
        warnings.filterwarnings("ignore", category=pyfits.verify.VerifyWarning)
        im = galsim.fits.read(file_name)

    assert im.bounds == ref_bounds
    assert im.wcs == ref_wcs
예제 #3
0
def getWCS(world_pos, PA=None, date=None, SCAs=None, PA_is_FPA=False):
    """
    This routine returns a dict containing a WCS for each of the WFIRST SCAs (Sensor Chip Array, the
    equivalent of a chip in an optical CCD).  The WFIRST SCAs are labeled 1-18, so these numbers are
    used as the keys in the dict.  Alternatively the user can request a subset of the SCAs using the
    `SCAs` option.  The basic instrument parameters used to create the WCS correspond to those in
    Cycle 6, which includes some significant updates from Cycle 5, including a 90 degree rotation of
    the focal plane axes relative to the payload axes, and two rows of SCAs are swapped.

    The user must specify a position for observation, at which the center of the focal plane array
    will point.  This must be supplied as a CelestialCoord `world_pos`.  In general, only certain
    positions are observable on certain dates, and for a given position there is an optimal position
    angle for the observatory (with the solar panels pointed as directly towards the sun as
    possible).  Users who are knowledgable about these details may choose to supply a position angle
    as `PA`, either for the observatory or for the focal plane (using `PA_is_FPA` to indicate this).
    But otherwise, the routine will simply choose the optimal position angle for a given date.

    To fully understand all possible inputs and outputs to this routine, users may wish to consult
    the diagram on the GalSim wiki,
    https://github.com/GalSim-developers/GalSim/wiki/GalSim-WFIRST-module-diagrams

    @param world_pos A galsim.CelestialCoord indicating the position to observe at the center of the
                     focal plane array (FPA).  Note that if the given position is not observable on
                     the given date, then the routine will raise an exception.
    @param PA        galsim.Angle representing the position angle of the observatory +Y axis, unless
                     `PA_is_FPA=True`, in which case it's the position angle of the FPA.  For users
                     to do not care about this, then leaving this as None will result in the routine
                     using the supplied `date` and `world_pos` to select the optimal orientation for
                     the observatory.  Note that if a user supplies a `PA` value, the routine does
                     not check whether this orientation is actually allowed.  [default: None]
    @param date      The date of the observation, as a python datetime object.  If None, then the
                     vernal equinox in 2025 will be used.  [default: None]
    @param PA_is_FPA If True, then the position angle that was provided was the PA of the focal
                     plane array, not the observatory. [default: False]
    @param SCAs      A single number or iterable giving the SCAs for which the WCS should be
                     obtained.  If None, then the WCS is calculated for all SCAs.
                     [default: None]
    @returns a dict of WCS objects for each SCA.
    """
    # Further gory details on coordinate systems, for developers: Observatory coordinate system is
    # defined such that +X_obs points along the boresight into the sky, +Z_obs points towards the
    # Sun in the absence of a roll offset (i.e., roll offset = 0 defines the optimal position angle
    # for the observatory), +Y_obs makes a right-handed system.
    #
    # Payload coordinate system: +X_pl points along -Y_obs, +Y_pl points along +Z_obs, +Z_pl points
    # along -X_obs (back towards observer).
    #
    # Wide field imager (WFI) focal plane assembly (FPA) coordinate system: This is defined by a
    # left-handed system f1, f2, that is rotated by an angle `theta_fpa` with respect to the payload
    # axes.  +f1 points along the long axis of the focal plane, transverse to the radius from the
    # telescope optic axis.  +f2 points radially out from the telescope optic axis, along the narrow
    # dimension of the focal plane.  If +f2 points North, then +f1 points East.  `theta_fpa` is a
    # positive CCW rotation of the f2 axis relative to -Y_pl, and of f1 relative to +X_pl.  In terms
    # of focal plane geometry, if +Y_fp is pointing North, then SCAs 3 and 12 will be at highest
    # declination, 8 and 17 at the lowest.  +Y_fp is aligned with the short axis of the focal plane
    # array.
    #
    # There is also a detector coordinate system (P1, P2).  +P1 and +P2 point along the fast- and
    # slow-scan directions of the pixel readout, respectively.
    #
    # So, for reference, if the boresight is pointed at RA=90, DEC=0 on March 21st (Sun at vernal
    # equinox), then +X_obs points at (RA,DEC)=(90,0), +Y_obs points North, and +Z_obs points at the
    # Sun.  The payload coordinates are +X_pl points South, -Y_pl points East.  Finally, the FPA
    # coordinate system is defined by +f2 being at a position angle 90+theta_fpa east of North.  If
    # the observatory +Y axis is at a position angle `pa_obsy` East of North, then the focal plane
    # (+f2) is at a position angle pa_fpa = pa_obsy + 90 + theta_fpa.

    # Parse input position
    if not isinstance(world_pos, galsim.CelestialCoord):
        raise TypeError(
            "Position on the sky must be given as a galsim.CelestialCoord!")

    # Get the date. (Vernal equinox in 2025, taken from
    # http://www.astropixels.com/ephemeris/soleq2001.html, if none was supplied.)
    if date is None:
        import datetime
        date = datetime.datetime(2025, 3, 20, 9, 2, 0)

    # Are we allowed to look here?
    if not allowedPos(world_pos, date):
        raise RuntimeError(
            "Error, WFIRST cannot look at this position on this date!")

    # If position angle was not given, then get the optimal one:
    if PA is None:
        PA_is_FPA = False
        PA = bestPA(world_pos, date)
    else:
        # Just enforce type
        if not isinstance(PA, galsim.Angle):
            raise TypeError("Position angle must be a galsim.Angle!")

    # Check which SCAs are to be done using a helper routine in this module.
    SCAs = galsim.wfirst._parse_SCAs(SCAs)

    # Compute position angle of FPA f2 axis, where positive corresponds to the angle east of North.
    if PA_is_FPA:
        pa_fpa = PA
        pa_obsy = PA - 90. * galsim.degrees - theta_fpa
    else:
        pa_obsy = PA
        pa_fpa = PA + 90. * galsim.degrees + theta_fpa
    cos_pa = np.cos(pa_fpa)
    sin_pa = np.sin(pa_fpa)

    # Figure out tangent-plane positions for FPA center:
    xc_fpa_tp, yc_fpa_tp = _det_to_tangplane_positions(xc_fpa, yc_fpa)

    # Note, this routine reads in the coeffs.  We don't use them until later, but read them in for
    # all SCAs at once.
    a_sip, b_sip = _parse_sip_file(sip_filename)

    # Loop over SCAs:
    wcs_dict = {}
    for i_sca in SCAs:
        # Set up the header.
        header = galsim.FitsHeader()
        # Populate some necessary variables in the FITS header that are always the same, regardless of
        # input and SCA number.
        _populate_required_fields(header)

        # And populate some things that just depend on the overall locations or other input, not on
        # the SCA.
        header['RA_TARG'] = (world_pos.ra / galsim.degrees,
                             "right ascension of the target (deg) (J2000)")
        header['DEC_TARG'] = (world_pos.dec / galsim.degrees,
                              "declination of the target (deg) (J2000)")
        header['PA_OBSY'] = (pa_obsy / galsim.degrees,
                             "position angle of observatory Y axis (deg)")
        header['PA_FPA'] = (pa_fpa / galsim.degrees,
                            "position angle of FPA Y axis (deg)")

        # Finally do all the SCA-specific stuff.
        header['SCA_NUM'] = (i_sca, "SCA number (1 - 18)")

        # Set the position of center of this SCA in focal plane angular coordinates.
        sca_xc_fpa = np.arctan(
            sca_xc_mm[i_sca] / focal_length) * galsim.radians
        sca_yc_fpa = np.arctan(
            sca_yc_mm[i_sca] / focal_length) * galsim.radians

        # Figure out tangent plane positions after distortion, and subtract off those for FPA center
        # (calculated in header).
        sca_xc_tp, sca_yc_tp = _det_to_tangplane_positions(
            sca_xc_fpa, sca_yc_fpa)
        # These define the tangent plane (X, Y) distance of the center of this SCA from the center
        # of the overall FPA.
        sca_xc_tp_f = sca_xc_tp - xc_fpa_tp
        sca_yc_tp_f = sca_yc_tp - yc_fpa_tp

        # Leave phi_p at 180 (0 if dec_targ==-90), so that tangent plane axes remain oriented along
        # celestial coordinates. In other words, phi_p is the angle of the +Y axis in the tangent
        # plane, which is of course pi if we're measuring these phi angles clockwise from the -Y
        # axis.  Note that this quantity is not used in any calculations at all, but for consistency
        # with the WCS code that comes from the WFIRST project office, we calculate this quantity
        # and put it in the FITS header.
        if world_pos.dec / galsim.degrees > -90.:
            phi_p = np.pi * galsim.radians
        else:
            phi_p = 0. * galsim.radians

        # Go from the tangent plane position of the SCA center, to the actual celestial coordinate,
        # using `world_pos` as the center point of the tangent plane projection.  This celestial
        # coordinate for the SCA center is `crval`, which goes into the WCS as CRVAL1, CRVAL2.
        u = -sca_xc_tp_f * cos_pa - sca_yc_tp_f * sin_pa
        v = -sca_xc_tp_f * sin_pa + sca_yc_tp_f * cos_pa
        crval = world_pos.deproject(galsim.PositionD(u / galsim.arcsec,
                                                     v / galsim.arcsec),
                                    projection='gnomonic')
        crval1 = crval.ra
        crval2 = crval.dec
        header['CRVAL1'] = (crval1 / galsim.degrees,
                            "first axis value at reference pixel")
        header['CRVAL2'] = (crval2 / galsim.degrees,
                            "second axis value at reference pixel")

        # Compute the position angle of the local pixel Y axis.
        # This requires projecting local North onto the detector axes.
        # Start by adding any SCA-unique rotation relative to FPA axes:
        sca_tp_rot = pa_fpa + sca_rot[i_sca] * galsim.degrees

        # Go some reasonable distance from crval in the +y direction.  Say, 1 degree.
        plus_y = world_pos.deproject(galsim.PositionD(
            u / galsim.arcsec, v / galsim.arcsec + 3600),
                                     projection='gnomonic')
        # Find the angle between this point, crval and due north.
        north = galsim.CelestialCoord(0. * galsim.degrees,
                                      90. * galsim.degrees)
        pa_sca = sca_tp_rot - crval.angleBetween(plus_y, north)

        # Compute CD coefficients: extract the linear terms from the a_sip, b_sip arrays.  These
        # linear terms are stored in the SIP arrays for convenience, but are defined differently.
        # The other terms have been divided by the linear terms, so that these become pure
        # multiplicative factors. There is no need to change signs of the SIP coefficents associated
        # with odd powers of X! Change sign of a10, b10 because the tangent-plane X pixel coordinate
        # has sign opposite to the detector pixel X coordinate, and this transformation maps pixels
        # to tangent plane.
        a10 = -a_sip[i_sca, 1, 0]
        a11 = a_sip[i_sca, 0, 1]
        b10 = -b_sip[i_sca, 1, 0]
        b11 = b_sip[i_sca, 0, 1]

        # Rotate by pa_fpa.
        cos_pa_sca = np.cos(pa_sca)
        sin_pa_sca = np.sin(pa_sca)
        header['CD1_1'] = (cos_pa_sca * a10 + sin_pa_sca * b10,
                           "partial of first axis coordinate w.r.t. x")
        header['CD1_2'] = (cos_pa_sca * a11 + sin_pa_sca * b11,
                           "partial of first axis coordinate w.r.t. y")
        header['CD2_1'] = (-sin_pa_sca * a10 + cos_pa_sca * b10,
                           "partial of second axis coordinate w.r.t. x")
        header['CD2_2'] = (-sin_pa_sca * a11 + cos_pa_sca * b11,
                           "partial of second axis coordinate w.r.t. y")
        header['ORIENTAT'] = (pa_sca / galsim.degrees,
                              "position angle of image y axis (deg. e of n)")
        header['LONPOLE'] = (phi_p / galsim.degrees,
                             "Native longitude of celestial pole")
        for i in range(n_sip):
            for j in range(n_sip):
                if i + j >= 2 and i + j < n_sip:
                    sipstr = "A_%d_%d" % (i, j)
                    header[sipstr] = a_sip[i_sca, i, j]
                    sipstr = "B_%d_%d" % (i, j)
                    header[sipstr] = b_sip[i_sca, i, j]

        wcs = galsim.GSFitsWCS(header=header)
        # Store the original header as an attribute of the WCS.  This ensures that we have all the
        # extra keywords for whenever an image with this WCS is written to file.
        wcs.header = header
        wcs_dict[i_sca] = wcs

    return wcs_dict
예제 #4
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')
예제 #5
0
import numpy as np
import galsim
import astropy.io.fits as fits

fns = ["sipsample.fits", "tpv.fits", "tanpv.fits"]

rng = np.random.default_rng()

size = 1_000_000

for fn in fns:
    header = fits.getheader(f"../tests/fits_files/{fn}")
    x = rng.uniform(0, header['NAXIS1'], size=size)
    y = rng.uniform(0, header['NAXIS2'], size=size)

    print()
    print(fn)
    print(f"PV?  {'PV1_1' in header}")
    print(f"SIP?  {'A_ORDER' in header}")

    wcs = galsim.GSFitsWCS(header=header)
    t0 = time.time()
    ra, dec = wcs.xyToradec(x, y, units='rad')
    t1 = time.time()
    print(f"xyToradec {t1-t0:.3f}")

    t0 = time.time()
    x1, y1 = wcs.radecToxy(ra, dec, units='rad')
    t1 = time.time()
    print(f"radecToxy {t1-t0:.3f}")