Example #1
0
def test_galsim_error():
    """Test basic usage of GalSimError
    """
    err = galsim.GalSimError("Test")
    print('str = ', str(err))
    print('repr = ', repr(err))
    assert str(err) == "Test"
    assert isinstance(err, RuntimeError)
    do_pickle(err)
Example #2
0
 def done_func(logger, proc, k, result, t2):
     file_num, file_name = info[k]
     file_name2, t = result  # This is the t for which 0 means the file was skipped.
     if file_name2 != file_name:  # pragma: no cover  (I think this should never happen.)
         raise galsim.GalSimError("Files seem to be out of sync. %s != %s",
                                  file_name, file_name2)
     if t != 0 and logger:
         if proc is None: s0 = ''
         else: s0 = '%s: ' % proc
         logger.warning(s0 + 'File %d = %s: time = %f sec', file_num,
                        file_name, t)
Example #3
0
    def writeHdu(self, config, base, logger):
        """Write the data to a FITS HDU with the data for this output object.

        The base class implementation is appropriate for the cas that the result of finalize
        is a list of images of length 1 to be written to a FITS file.

        @param config       The configuration field for this output object.
        @param base         The base configuration dict.
        @param logger       If given, a logger object to log progress. [default: None]

        @returns an HDU with the output data.
        """
        if len(self.data) != 1:  # pragma: no cover  (Not sure if this is possible.)
            raise galsim.GalSimError(
                    "%d %s images were created. Expecting 1."%(n,self._extra_output_key))
        return self.data[0]
Example #4
0
def _parse_WCS_inputs(world_pos, PA, date, PA_is_FPA, SCAs):
    """
    This routine parses the various input options to getWCS() and returns what the routine needs to
    do its job.  The reason to pull this out is so other helper routines can use it.
    """
    # 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 galsim.GalSimError("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 the galsim.wfirst 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

    return date, SCAs, pa_fpa, pa_obsy
Example #5
0
def getSkyLevel(bandpass, world_pos=None, exptime=None, epoch=2025, date=None):
    """
    Get the expected sky level for a WFIRST observation due to zodiacal light for this bandpass and
    position.

    This routine requires Bandpass objects that were loaded by galsim.wfirst.getBandpasses().  That
    routine will have stored tables containing the sky background as a function of position on the
    sky for that bandpass.  This routine then interpolates between the values in those tables to
    arbitrary positions on the sky.

    The numbers that are stored in the Bandpass object `bandpass` are background level in units of
    e-/m^2/s/arcsec^2.  To get rid of the m^2, this routine multiplies by the total effective
    collecting area in m^2.  Multiplying by the exposure time gives a result in e-/arcsec^2.  The
    result can either be multiplied by the approximate pixel area to get e-/pix, or the result can
    be used with wcs.makeSkyImage() to make an image of the sky that properly includes the actual
    pixel area as a function of position on the detector.

    The source of the tables that are being interpolated is Chris Hirata's publicly-available WFIRST
    exposure time calculator (ETC):

        http://www.tapir.caltech.edu/~chirata/web/software/space-etc/

    It nominally returns photons/m^2/s/arcsec^2, but the input bandpasses used internally by the ETC
    code include the quantum efficiency, to effectively convert to e-/m^2/s/arcsec^2.  Note that in
    general results will depend on the adopted model for zodiacal light, and these are uncertain at
    the ~10% level.

    Positions should be specified with the `world_pos` keyword, which must be a CelestialCoord
    object.  If no `world_pos` is supplied, then the routine will use a default position that looks
    sensibly away from the sun.

    @param bandpass     A Bandpass object.
    @param world_pos    A position, given as a CelestialCoord object.  If None, then the routine
                        will use an ecliptic longitude of 90 degrees with respect to the sun
                        position (as a fair compromise between 0 and 180), and an ecliptic latitude
                        of 30 degrees with respect to the sun position (decently out of the plane
                        of the Earth-sun orbit). [default: None]
    @param exptime      Exposure time in seconds.  If None, use the default WFIRST exposure time.
                        [default: None]
    @param epoch        The epoch to be used for estimating the obliquity of the ecliptic when
                        converting `world_pos` to ecliptic coordinates.  This keyword is only used
                        if `date` is None, otherwise `date` is used to determine the `epoch`.
                        [default: 2025]
    @param date         The date of the observation, provided as a python datetime object.  If None,
                        then the conversion to ecliptic coordinates assumes the sun is at ecliptic
                        coordinates of (0,0), as it is at the vernal equinox. [default: None]

    @returns the expected sky level in e-/arcsec^2.
    """
    # Check for cached sky level information for this filter.  If not, raise exception
    if not hasattr(bandpass, '_sky_level'):
        raise galsim.GalSimError(
            "Only bandpasses returned from galsim.wfirst.getBandpasses() are "
            "allowed here!")

    # Check for proper type for position, and extract the ecliptic coordinates.
    if world_pos is None:
        # Use our defaults for the case of unspecified position.
        ecliptic_lat = 30. * galsim.degrees
        ecliptic_lon = 90. * galsim.degrees
    else:
        if not isinstance(world_pos, galsim.CelestialCoord):
            raise TypeError("world_pos must be supplied as a CelestialCoord.")
        if date is not None:
            epoch = date.year
        ecliptic_lon, ecliptic_lat = world_pos.ecliptic(epoch=epoch, date=date)

    # Check the position in our table, and make sure to take advantage of the latitude / longitude
    # symmetries:
    # The table only includes positive values of latitude, because there is symmetry about zero.  So
    # we take the absolute value of the input ecliptic latitude.
    # The table only includes longitude in the range [0, 180] because there is symmetry in that a
    # negative longitude in the range[-180, 0] should have the same sky level as at the positive
    # value of longitude (given that the Sun is at 0).
    ecliptic_lon = ecliptic_lon.wrap()
    ecliptic_lon = abs(ecliptic_lon.rad) * galsim.radians
    ecliptic_lat = abs(ecliptic_lat.rad) * galsim.radians
    sin_ecliptic_lat = np.sin(ecliptic_lat)

    # Take the lookup table, and turn negative numbers (indicating failure because of proximity to
    # sun) to large positive values so that we can identify them as bad after interpolation.
    max_sky = np.max(bandpass._sky_level)
    sky_level = bandpass._sky_level.copy()
    sky_level[sky_level < 0] = 1.e6

    # Interpolate in 2d on the table.
    s = sky_level.reshape(46, 42).transpose()
    xlat = sin_ecliptic_lat * 41
    xlon = abs(ecliptic_lon.wrap() / galsim.degrees) / 4.
    ilat = int(xlat)
    ilon = int(xlon)
    xlat -= ilat
    xlon -= ilon
    sky_val = (s[ilat, ilon] * (1. - xlat) * (1. - xlon) + s[ilat, ilon + 1] *
               (1. - xlat) * xlon + s[ilat + 1, ilon] * xlat * (1. - xlon) +
               s[ilat + 1, ilon + 1] * xlat * xlon)

    # If the result is too large, then raise an exception: we should not look at this position!
    if sky_val > max_sky:
        raise galsim.GalSimValueError(
            "world_pos is too close to sun. Would not observe here.",
            world_pos)

    # Now, convert to the right units, and return.  (See docstring for explanation.)
    # First, multiply by the effective collecting area in m^2.
    eff_area = 0.25 * np.pi * galsim.wfirst.diameter**2 * (
        1. - galsim.wfirst.obscuration**2)
    sky_val *= eff_area
    # Multiply by exposure time.
    if exptime is None:
        exptime = galsim.wfirst.exptime
    sky_val *= exptime

    # The result is now the sky level in e-/arcsec^2.
    return sky_val
Example #6
0
def WriteMEDS(obj_list, file_name, clobber=True):
    """
    Writes a MEDS file from a list of `MultiExposureObject` instances.

    Parameters:
       obj_list:    List of `MultiExposureObject` instances
       file_name:   Name of meds file to be written
       clobber:     Setting ``clobber=True`` when ``file_name`` is given will silently overwrite
                    existing files. [default True]
    """

    from galsim._pyfits import pyfits

    # initialise the catalog
    cat = {}
    cat['id'] = []
    cat['box_size'] = []
    cat['ra'] = []
    cat['dec'] = []
    cat['ncutout'] = []
    cat['start_row'] = []
    cat['dudrow'] = []
    cat['dudcol'] = []
    cat['dvdrow'] = []
    cat['dvdcol'] = []
    cat['orig_start_row'] = []
    cat['orig_start_col'] = []
    cat['cutout_row'] = []
    cat['cutout_col'] = []
    cat['psf_box_size'] = []
    cat['psf_start_row'] = []

    # initialise the image vectors
    vec = {}
    vec['image'] = []
    vec['seg'] = []
    vec['weight'] = []
    vec['psf'] = []

    # initialise the image vector index
    n_vec = 0
    psf_n_vec = 0

    # get number of objects
    n_obj = len(obj_list)

    # loop over objects
    for obj in obj_list:

        # initialise the start indices for each image
        start_rows = np.ones(MAX_NCUTOUTS) * EMPTY_START_INDEX
        psf_start_rows = np.ones(MAX_NCUTOUTS) * EMPTY_START_INDEX
        dudrow = np.ones(MAX_NCUTOUTS) * EMPTY_JAC_diag
        dudcol = np.ones(MAX_NCUTOUTS) * EMPTY_JAC_offdiag
        dvdrow = np.ones(MAX_NCUTOUTS) * EMPTY_JAC_offdiag
        dvdcol = np.ones(MAX_NCUTOUTS) * EMPTY_JAC_diag
        cutout_row = np.ones(MAX_NCUTOUTS) * EMPTY_SHIFT
        cutout_col = np.ones(MAX_NCUTOUTS) * EMPTY_SHIFT
        # get the number of cutouts (exposures)
        n_cutout = obj.n_cutouts

        # append the catalog for this object
        cat['id'].append(obj.id)
        cat['box_size'].append(obj.box_size)
        # TODO: If the config defines a world position, get the right ra, dec here.
        cat['ra'].append(0.)
        cat['dec'].append(0.)
        cat['ncutout'].append(n_cutout)
        cat['psf_box_size'].append(obj.psf_box_size)

        # loop over cutouts
        for i in range(n_cutout):

            # assign the start row to the end of image vector
            start_rows[i] = n_vec
            psf_start_rows[i] = psf_n_vec
            # update n_vec to point to the end of image vector
            n_vec += len(obj.images[i].array.flatten())
            if obj.psf is not None:
                psf_n_vec += len(obj.psf[i].array.flatten())

            # append the image vectors
            vec['image'].append(obj.images[i].array.flatten())
            vec['seg'].append(obj.seg[i].array.flatten())
            vec['weight'].append(obj.weight[i].array.flatten())
            if obj.psf is not None:
                vec['psf'].append(obj.psf[i].array.flatten())

            # append cutout_row/col
            cutout_row[i] = obj.cutout_row[i]
            cutout_col[i] = obj.cutout_col[i]

            # append the Jacobian
            # col == x
            # row == y
            dudcol[i] = obj.wcs[i].dudx
            dudrow[i] = obj.wcs[i].dudy
            dvdcol[i] = obj.wcs[i].dvdx
            dvdrow[i] = obj.wcs[i].dvdy

            # check if we are running out of memory
            if sys.getsizeof(vec) > MAX_MEMORY:  # pragma: no cover
                raise galsim.GalSimError(
                    "Running out of memory > %1.0fGB - you can increase the limit by changing "
                    "galsim.des_meds.MAX_MEMORY" % (MAX_MEMORY / 1.e9))

        # update the start rows fields in the catalog
        cat['start_row'].append(start_rows)
        cat['psf_start_row'].append(psf_start_rows)

        # add cutout_row/col
        cat['cutout_row'].append(cutout_row)
        cat['cutout_col'].append(cutout_col)

        # add lists of Jacobians
        cat['dudrow'].append(dudrow)
        cat['dudcol'].append(dudcol)
        cat['dvdrow'].append(dvdrow)
        cat['dvdcol'].append(dvdcol)

    # concatenate list to one big vector
    vec['image'] = np.concatenate(vec['image'])
    vec['seg'] = np.concatenate(vec['seg'])
    vec['weight'] = np.concatenate(vec['weight'])
    if obj.psf is not None:
        vec['psf'] = np.concatenate(vec['psf'])

    # get the primary HDU
    primary = pyfits.PrimaryHDU()

    # second hdu is the object_data
    # cf. https://github.com/esheldon/meds/wiki/MEDS-Format
    cols = []
    cols.append(pyfits.Column(name='id', format='K', array=cat['id']))
    cols.append(pyfits.Column(name='number', format='K', array=cat['id']))
    cols.append(pyfits.Column(name='ra', format='D', array=cat['ra']))
    cols.append(pyfits.Column(name='dec', format='D', array=cat['dec']))
    cols.append(
        pyfits.Column(name='box_size', format='K', array=cat['box_size']))
    cols.append(pyfits.Column(name='ncutout', format='K',
                              array=cat['ncutout']))
    cols.append(
        pyfits.Column(name='file_id',
                      format='%dK' % MAX_NCUTOUTS,
                      array=[1] * n_obj))
    cols.append(
        pyfits.Column(name='start_row',
                      format='%dK' % MAX_NCUTOUTS,
                      array=np.array(cat['start_row'])))
    cols.append(
        pyfits.Column(name='orig_row',
                      format='%dD' % MAX_NCUTOUTS,
                      array=[[0] * MAX_NCUTOUTS] * n_obj))
    cols.append(
        pyfits.Column(name='orig_col',
                      format='%dD' % MAX_NCUTOUTS,
                      array=[[0] * MAX_NCUTOUTS] * n_obj))
    cols.append(
        pyfits.Column(name='orig_start_row',
                      format='%dK' % MAX_NCUTOUTS,
                      array=[[0] * MAX_NCUTOUTS] * n_obj))
    cols.append(
        pyfits.Column(name='orig_start_col',
                      format='%dK' % MAX_NCUTOUTS,
                      array=[[0] * MAX_NCUTOUTS] * n_obj))
    cols.append(
        pyfits.Column(name='cutout_row',
                      format='%dD' % MAX_NCUTOUTS,
                      array=np.array(cat['cutout_row'])))
    cols.append(
        pyfits.Column(name='cutout_col',
                      format='%dD' % MAX_NCUTOUTS,
                      array=np.array(cat['cutout_col'])))
    cols.append(
        pyfits.Column(name='dudrow',
                      format='%dD' % MAX_NCUTOUTS,
                      array=np.array(cat['dudrow'])))
    cols.append(
        pyfits.Column(name='dudcol',
                      format='%dD' % MAX_NCUTOUTS,
                      array=np.array(cat['dudcol'])))
    cols.append(
        pyfits.Column(name='dvdrow',
                      format='%dD' % MAX_NCUTOUTS,
                      array=np.array(cat['dvdrow'])))
    cols.append(
        pyfits.Column(name='dvdcol',
                      format='%dD' % MAX_NCUTOUTS,
                      array=np.array(cat['dvdcol'])))
    cols.append(
        pyfits.Column(name='psf_box_size',
                      format='K',
                      array=cat['psf_box_size']))
    cols.append(
        pyfits.Column(name='psf_start_row',
                      format='%dK' % MAX_NCUTOUTS,
                      array=np.array(cat['psf_start_row'])))

    # Depending on the version of pyfits, one of these should work:
    try:
        object_data = pyfits.BinTableHDU.from_columns(cols)
        object_data.name = 'object_data'
    except AttributeError:  # pragma: no cover
        object_data = pyfits.new_table(pyfits.ColDefs(cols))
        object_data.update_ext_name('object_data')

    # third hdu is image_info
    cols = []
    cols.append(
        pyfits.Column(name='image_path',
                      format='A256',
                      array=['generated_by_galsim']))
    cols.append(pyfits.Column(name='image_ext', format='I', array=[0]))
    cols.append(
        pyfits.Column(name='weight_path',
                      format='A256',
                      array=['generated_by_galsim']))
    cols.append(pyfits.Column(name='weight_ext', format='I', array=[0]))
    cols.append(
        pyfits.Column(name='seg_path',
                      format='A256',
                      array=['generated_by_galsim']))
    cols.append(pyfits.Column(name='seg_ext', format='I', array=[0]))
    cols.append(
        pyfits.Column(name='bmask_path',
                      format='A256',
                      array=['generated_by_galsim']))
    cols.append(pyfits.Column(name='bmask_ext', format='I', array=[0]))
    cols.append(
        pyfits.Column(name='bkg_path',
                      format='A256',
                      array=['generated_by_galsim']))
    cols.append(pyfits.Column(name='bkg_ext', format='I', array=[0]))
    cols.append(pyfits.Column(name='image_id', format='K', array=[-1]))
    cols.append(pyfits.Column(name='image_flags', format='K', array=[-1]))
    cols.append(pyfits.Column(name='magzp', format='E', array=[30.]))
    cols.append(pyfits.Column(name='scale', format='E', array=[1.]))
    # TODO: Not sure if this is right!
    cols.append(pyfits.Column(name='position_offset', format='D', array=[0.]))
    try:
        image_info = pyfits.BinTableHDU.from_columns(cols)
        image_info.name = 'image_info'
    except AttributeError:  # pragma: no cover
        image_info = pyfits.new_table(pyfits.ColDefs(cols))
        image_info.update_ext_name('image_info')

    # fourth hdu is metadata
    # default values?
    cols = []
    cols.append(pyfits.Column(name='magzp_ref', format='E', array=[30.]))
    cols.append(
        pyfits.Column(name='DESDATA',
                      format='A256',
                      array=['generated_by_galsim']))
    cols.append(
        pyfits.Column(name='cat_file',
                      format='A256',
                      array=['generated_by_galsim']))
    cols.append(
        pyfits.Column(name='coadd_image_id',
                      format='A256',
                      array=['generated_by_galsim']))
    cols.append(
        pyfits.Column(name='coadd_file',
                      format='A256',
                      array=['generated_by_galsim']))
    cols.append(pyfits.Column(name='coadd_hdu', format='K', array=[9999]))
    cols.append(pyfits.Column(name='coadd_seg_hdu', format='K', array=[9999]))
    cols.append(
        pyfits.Column(name='coadd_srclist',
                      format='A256',
                      array=['generated_by_galsim']))
    cols.append(pyfits.Column(name='coadd_wt_hdu', format='K', array=[9999]))
    cols.append(
        pyfits.Column(name='coaddcat_file',
                      format='A256',
                      array=['generated_by_galsim']))
    cols.append(
        pyfits.Column(name='coaddseg_file',
                      format='A256',
                      array=['generated_by_galsim']))
    cols.append(
        pyfits.Column(name='cutout_file',
                      format='A256',
                      array=['generated_by_galsim']))
    cols.append(pyfits.Column(name='max_boxsize', format='A3', array=['-1']))
    cols.append(pyfits.Column(name='medsconf', format='A3', array=['x']))
    cols.append(pyfits.Column(name='min_boxsize', format='A2', array=['-1']))
    cols.append(pyfits.Column(name='se_badpix_hdu', format='K', array=[9999]))
    cols.append(pyfits.Column(name='se_hdu', format='K', array=[9999]))
    cols.append(pyfits.Column(name='se_wt_hdu', format='K', array=[9999]))
    cols.append(pyfits.Column(name='seg_hdu', format='K', array=[9999]))
    cols.append(pyfits.Column(name='psf_hdu', format='K', array=[9999]))
    cols.append(pyfits.Column(name='sky_hdu', format='K', array=[9999]))
    cols.append(pyfits.Column(name='fake_coadd_seg', format='K', array=[9999]))
    try:
        metadata = pyfits.BinTableHDU.from_columns(cols)
        metadata.name = 'metadata'
    except AttributeError:  # pragma: no cover
        metadata = pyfits.new_table(pyfits.ColDefs(cols))
        metadata.update_ext_name('metadata')

    # rest of HDUs are image vectors
    image_cutouts = pyfits.ImageHDU(vec['image'], name='image_cutouts')
    weight_cutouts = pyfits.ImageHDU(vec['weight'], name='weight_cutouts')
    seg_cutouts = pyfits.ImageHDU(vec['seg'], name='seg_cutouts')

    hdu_list = [
        primary,
        object_data,
        image_info,
        metadata,
        image_cutouts,
        weight_cutouts,
        seg_cutouts,
    ]

    if obj.psf is not None:
        psf_cutouts = pyfits.ImageHDU(vec['psf'], name='psf')
        hdu_list.append(psf_cutouts)

    galsim.fits.writeFile(file_name, pyfits.HDUList(hdu_list))
Example #7
0
def storePSFImages(PSF_dict, filename, bandpass_list=None, clobber=False):
    """
    This is a routine to store images of chromatic WFIRST PSFs in different bands for each SCA.  It
    takes an output dict of PSFs (`PSF_dict`) directly from getPSF().  The output will be a file
    (`filename`) that has all the images, along with an HDU that contains a FITS table indicating
    the bandpasses, SCAs, and other information needed to reconstruct the PSF information.

    This routine is not meant to work for PSFs from getPSF() that are completely achromatic.  The
    reason for this is that those PSFs are quite fast to generate, so there is little benefit to
    storing them.

    @param PSF_dict            A dict of PSF objects for each SCA, in the same format as output by
                               the getPSF() routine (though it can take versions that have been
                               modified, for example in the inclusion of an SED).
    @param filename            The name of the file to which the images and metadata should be
                               written; extension should be *.fits.
    @param bandpass_list       A list of bandpass names for which images should be generated and
                               stored.  If None, all WFIRST imaging passbands are used.
                               [default: None]
    @param clobber             Should the routine clobber `filename` (if they already exist)?
                               [default: False]
    """
    from galsim._pyfits import pyfits
    # Check for sane input PSF_dict.
    if len(PSF_dict) == 0 or len(PSF_dict) > galsim.wfirst.n_sca or \
            min(PSF_dict.keys()) < 1 or max(PSF_dict.keys()) > galsim.wfirst.n_sca:
        raise galsim.GalSimError("PSF_dict must come from getPSF().")

    # Check if file already exists and warn about clobbering.
    if os.path.isfile(filename):
        if clobber:
            os.remove(filename)
        else:
            raise OSError("Output file %r already exists" % filename)

    # Check that bandpass list input is okay.  It should be strictly a subset of the default list of
    # bandpasses.
    if bandpass_list is None:
        bandpass_list = default_bandpass_list
    else:
        if not isinstance(bandpass_list[0], basestring):
            raise TypeError("Expected input list of bandpass names.")
        if not set(bandpass_list).issubset(default_bandpass_list):
            raise galsim.GalSimValueError("Invalid values in bandpass_list",
                                          bandpass_list, default_bandpass_list)

    # Get all the WFIRST bandpasses.
    bandpass_dict = galsim.wfirst.getBandpasses()

    # Loop through making images and lists of their relevant parameters.
    im_list = []
    bp_name_list = []
    SCA_index_list = []
    for SCA in PSF_dict:
        PSF = PSF_dict[SCA]
        if not isinstance(PSF, galsim.ChromaticOpticalPSF) and \
                not isinstance(PSF, galsim.InterpolatedChromaticObject):
            raise galsim.GalSimValueError("PSFs are not ChromaticOpticalPSFs.",
                                          PSF_dict)
        star = galsim.Gaussian(sigma=1.e-8, flux=1.)

        for bp_name in bandpass_list:
            bandpass = bandpass_dict[bp_name]
            star_sed = galsim.SED(lambda x: 1, 'nm',
                                  'flambda').withFlux(1, bandpass)
            obj = galsim.Convolve(star * star_sed, PSF)

            im = obj.drawImage(bandpass,
                               scale=0.5 * galsim.wfirst.pixel_scale,
                               method='no_pixel')
            im_list.append(im)
            bp_name_list.append(bp_name)
            SCA_index_list.append(SCA)

    # Save images to file.
    galsim.fits.writeMulti(im_list, filename, clobber=clobber)

    # Add data to file, after constructing a FITS table.  Watch out for clobbering.
    bp_names = pyfits.Column(name='bandpass',
                             format='A10',
                             array=np.array(bp_name_list))
    SCA_indices = pyfits.Column(name='SCA',
                                format='J',
                                array=np.array(SCA_index_list))
    cols = pyfits.ColDefs([bp_names, SCA_indices])
    tbhdu = pyfits.BinTableHDU.from_columns(cols)
    f = pyfits.open(filename, mode='update')
    f.append(tbhdu)
    f.flush()
    f.close()
Example #8
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 galsim.GalSimError(
            "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 = []
        # 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.extend([
            ('RA_TARG', world_pos.ra / galsim.degrees,
             "right ascension of the target (deg) (J2000)"),
            ('DEC_TARG', world_pos.dec / galsim.degrees,
             "declination of the target (deg) (J2000)"),
            ('PA_OBSY', pa_obsy / galsim.degrees,
             "position angle of observatory Y axis (deg)"),
            ('PA_FPA', pa_fpa / galsim.degrees,
             "position angle of FPA Y axis (deg)"),
            ('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(u, v, projection='gnomonic')
        crval1 = crval.ra
        crval2 = crval.dec

        # 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(u,
                                     v + 1 * galsim.degrees,
                                     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.extend([
            ('CRVAL1', crval1 / galsim.degrees,
             "first axis value at reference pixel"),
            ('CRVAL2', crval2 / galsim.degrees,
             "second axis value at reference pixel"),
            ('CD1_1', cos_pa_sca * a10 + sin_pa_sca * b10,
             "partial of first axis coordinate w.r.t. x"),
            ('CD1_2', cos_pa_sca * a11 + sin_pa_sca * b11,
             "partial of first axis coordinate w.r.t. y"),
            ('CD2_1', -sin_pa_sca * a10 + cos_pa_sca * b10,
             "partial of second axis coordinate w.r.t. x"),
            ('CD2_2', -sin_pa_sca * a11 + cos_pa_sca * b11,
             "partial of second axis coordinate w.r.t. y"),
            ('ORIENTAT', pa_sca / galsim.degrees,
             "position angle of image y axis (deg. e of n)"),
            ('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.append((sipstr, a_sip[i_sca, i, j]))
                    sipstr = "B_%d_%d" % (i, j)
                    header.append((sipstr, b_sip[i_sca, i, j]))

        header = galsim.FitsHeader(header)
        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