Exemplo n.º 1
0
 def __init__(self, tiles_file=None):
     log = desiutil.log.get_logger()
     config = desisurvey.config.Configuration()
     # Read the specified tiles file.
     self.tiles_file = tiles_file or config.tiles_file()
     tiles = desimodel.io.load_tiles(
         onlydesi=True, extra=False, tilesfile=self.tiles_file)
     # Check for any unknown program names.
     tile_programs = np.unique(tiles['PROGRAM'])
     unknown = set(tile_programs) - set(self.PROGRAMS)
     if unknown:
         raise RuntimeError('Cannot schedule unknown program(s): {}.'.format(unknown))
     # Copy tile arrays.
     self.tileID = tiles['TILEID'].copy()
     self.passnum = tiles['PASS'].copy()
     self.tileRA = tiles['RA'].copy()
     self.tileDEC = tiles['DEC'].copy()
     # Count tiles.
     self.ntiles = len(self.tileID)
     self.pass_ntiles = {p: np.count_nonzero(self.passnum == p)
                         for p in np.unique(self.passnum)}
     # Get list of passes.
     self.passes = np.unique(self.passnum)
     self.npasses = len(self.passes)
     # Map each pass to a small integer index.
     self.pass_index = {p: idx for idx, p in enumerate(self.passes)}
     # Can remove this when tile_index no longer uses searchsorted.
     if not np.all(np.diff(self.tileID) > 0):
         raise RuntimeError('Tile IDs are not increasing.')
     # Build program -> [passes] maps. A program with no tiles will map to an empty array.
     self.program_passes = {
         p: np.unique(self.passnum[tiles['PROGRAM'] == p]) for p in self.PROGRAMS}
     # Build pass -> program maps.
     self.pass_program = {}
     for p in self.PROGRAMS:
         self.pass_program.update({passnum: p for passnum in self.program_passes[p]})
     # Build tile masks for each program. A program will no tiles with have an empty mask.
     self.program_mask = {}
     for p in self.PROGRAMS:
         mask = np.zeros(self.ntiles, bool)
         for pnum in self.program_passes[p]:
             mask |= (self.passnum == pnum)
         self.program_mask[p] = mask
     # Calculate and save dust exposure factors.
     self.dust_factor = desisurvey.etc.dust_exposure_factor(tiles['EBV_MED'])
     # Precompute coefficients to calculate tile observing airmass.
     latitude = np.radians(config.location.latitude())
     tile_dec_rad = np.radians(self.tileDEC)
     self.tile_coef_A = np.sin(tile_dec_rad) * np.sin(latitude)
     self.tile_coef_B = np.cos(tile_dec_rad) * np.cos(latitude)
     # Placeholders for overlap attributes that are expensive to calculate
     # so we use lazy evaluation the first time they are accessed.
     self._tile_over = None
     self._overlapping = None
     self._fiberassign_delay = None
Exemplo n.º 2
0
 def __init__(self, tiles_file=None):
     log = desiutil.log.get_logger()
     config = desisurvey.config.Configuration()
     # Read the specified tiles file.
     self.tiles_file = tiles_file or config.tiles_file()
     tiles = desimodel.io.load_tiles(
         onlydesi=True, extra=False, tilesfile=self.tiles_file)
     # Check for any unknown program names.
     tile_programs = np.unique(tiles['PROGRAM'])
     unknown = set(tile_programs) - set(self.PROGRAMS)
     if unknown:
         raise RuntimeError('Cannot schedule unknown program(s): {}.'.format(unknown))
     # Copy tile arrays.
     self.tileID = tiles['TILEID'].copy()
     self.passnum = tiles['PASS'].copy()
     self.tileRA = tiles['RA'].copy()
     self.tileDEC = tiles['DEC'].copy()
     # Count tiles.
     self.ntiles = len(self.tileID)
     self.pass_ntiles = {p: np.count_nonzero(self.passnum == p)
                         for p in np.unique(self.passnum)}
     # Get list of passes.
     self.passes = np.unique(self.passnum)
     self.npasses = len(self.passes)
     # Map each pass to a small integer index.
     self.pass_index = {p: idx for idx, p in enumerate(self.passes)}
     # Can remove this when tile_index no longer uses searchsorted.
     if not np.all(np.diff(self.tileID) > 0):
         raise RuntimeError('Tile IDs are not increasing.')
     # Build program -> [passes] maps. A program with no tiles will map to an empty array.
     self.program_passes = {
         p: np.unique(self.passnum[tiles['PROGRAM'] == p]) for p in self.PROGRAMS}
     # Build pass -> program maps.
     self.pass_program = {}
     for p in self.PROGRAMS:
         self.pass_program.update({passnum: p for passnum in self.program_passes[p]})
     # Build tile masks for each program. A program will no tiles with have an empty mask.
     self.program_mask = {}
     for p in self.PROGRAMS:
         mask = np.zeros(self.ntiles, bool)
         for pnum in self.program_passes[p]:
             mask |= (self.passnum == pnum)
         self.program_mask[p] = mask
     # Calculate and save dust exposure factors.
     self.dust_factor = desisurvey.etc.dust_exposure_factor(tiles['EBV_MED'])
     # Precompute coefficients to calculate tile observing airmass.
     latitude = np.radians(config.location.latitude())
     tile_dec_rad = np.radians(self.tileDEC)
     self.tile_coef_A = np.sin(tile_dec_rad) * np.sin(latitude)
     self.tile_coef_B = np.cos(tile_dec_rad) * np.cos(latitude)
     # Placeholders for overlap attributes that are expensive to calculate
     # so we use lazy evaluation the first time they are accessed.
     self._tile_over = None
     self._overlapping = None
     self._fiberassign_delay = None
Exemplo n.º 3
0
def get_tiles(tiles_file=None, use_cache=True, write_cache=True):
    """Return a Tiles object with optional caching.

    You should normally always use the default arguments to ensure
    that tiles are defined consistently and efficiently between
    different classes.

    Parameters
    ----------
    tiles_file : str or None
        Use the specified name to override config.tiles_file.
    use_cache : bool
        Use tiles previously cached in memory when True.
        Otherwise, (re)load tiles from disk.
    write_cache : bool
        If tiles need to be loaded from disk with this call,
        save them in a memory cache for future calls.
    """
    global _cached_tiles

    log = desiutil.log.get_logger()
    config = desisurvey.config.Configuration()
    tiles_file = tiles_file or config.tiles_file()

    if use_cache and tiles_file in _cached_tiles:
        tiles = _cached_tiles[tiles_file]
        log.debug('Using cached tiles for "{}".'.format(tiles_file))
    else:
        tiles = Tiles(tiles_file)
        log.info('Initialized tiles from "{}".'.format(tiles_file))
        for pname in Tiles.PROGRAMS:
            pinfo = []
            for passnum in tiles.program_passes[pname]:
                pinfo.append('{}({})'.format(passnum,
                                             tiles.pass_ntiles[passnum]))
            log.info('{:6s} passes(tiles): {}.'.format(pname,
                                                       ', '.join(pinfo)))

    if write_cache:
        _cached_tiles[tiles_file] = tiles
    else:
        log.info('Tiles not cached for "{}".'.format(tiles_file))

    return tiles
Exemplo n.º 4
0
def get_tiles(tiles_file=None, use_cache=True, write_cache=True):
    """Return a Tiles object with optional caching.

    You should normally always use the default arguments to ensure
    that tiles are defined consistently and efficiently between
    different classes.

    Parameters
    ----------
    tiles_file : str or None
        Use the specified name to override config.tiles_file.
    use_cache : bool
        Use tiles previously cached in memory when True.
        Otherwise, (re)load tiles from disk.
    write_cache : bool
        If tiles need to be loaded from disk with this call,
        save them in a memory cache for future calls.
    """
    global _cached_tiles

    log = desiutil.log.get_logger()
    config = desisurvey.config.Configuration()
    tiles_file = tiles_file or config.tiles_file()

    if use_cache and tiles_file in _cached_tiles:
        tiles = _cached_tiles[tiles_file]
        log.debug('Using cached tiles for "{}".'.format(tiles_file))
    else:
        tiles = Tiles(tiles_file)
        log.info('Initialized tiles from "{}".'.format(tiles_file))
        for pname in Tiles.PROGRAMS:
            pinfo = []
            for passnum in tiles.program_passes[pname]:
                pinfo.append('{}({})'.format(passnum, tiles.pass_ntiles[passnum]))
            log.info('{:6s} passes(tiles): {}.'.format(pname, ', '.join(pinfo)))

    if write_cache:
        _cached_tiles[tiles_file] = tiles
    else:
        log.info('Tiles not cached for "{}".'.format(tiles_file))

    return tiles
Exemplo n.º 5
0
    def get_exposures(self, start=None, stop=None,
                      tile_fields='tileid,pass,ra,dec,ebmv',
                      exp_fields=('night,mjd,exptime,seeing,transparency,' +
                                  'airmass,moonfrac,moonalt,moonsep,' +
                                  'program,flavor')):
        """Create a table listing exposures in time order.

        Parameters
        ----------
        start : date or None
            First date to include in the list of exposures, or date of the
            first observation if None.
        stop  : date or None
            Last date to include in the list of exposures, or date of the
            last observation if None.
        tile_fields : str
            Comma-separated list of per-tile field names to include. The
            special name 'index' denotes the index into the visible tile array.
            The special name 'ebmv' adds median E(B-V) values for each tile
            from the tile design file.
        exp_fields : str
            Comma-separated list of per-exposure field names to include. The
            special name 'snr2cum' denotes the cummulative snr2frac on each
            tile, since the start of the survey.  The special name 'night'
            denotes a string YYYYMMDD specifying the date on which each
            night starts. The special name 'lst' denotes the apparent local
            sidereal time of the shutter open timestamp. The special name
            'expid' denotes the index of each exposure in the full progress
            record starting from zero.

        Returns
        -------
        astropy.table.Table
            Table with the specified columns as uppercase and one row per exposure.
        """
        # Get MJD range to show.
        if start is None:
            start = self.first_mjd
        start = desisurvey.utils.local_noon_on_date(
            desisurvey.utils.get_date(start)).mjd
        if stop is None:
            stop = self.last_mjd
        stop = desisurvey.utils.local_noon_on_date(
            desisurvey.utils.get_date(stop)).mjd + 1
        if start >= stop:
            raise ValueError('Expected start < stop.')

        # Build a list of exposures in time sequence.
        table = self._table
        mjd = table['mjd'].data.flatten()
        order = np.argsort(mjd)
        tile_index = (order // self.max_exposures)

        # Assign each exposure a sequential index starting from zero.
        ntot = len(mjd)
        nexp = np.count_nonzero(mjd > 0)
        expid = np.empty(ntot, int)
        expid[order] = np.arange(nexp - ntot, nexp)

        # Restrict to the requested date range.
        first, last = np.searchsorted(mjd, [start, stop], sorter=order)
        tile_index = tile_index[first:last + 1]
        order = order[first:last + 1]
        assert np.all(expid[order] >= 0)

        # Create the output table.
        tileinfo = None
        output = astropy.table.Table()
        output.meta['EXTNAME'] = 'EXPOSURES'
        for name in tile_fields.split(','):
            name = name.lower()
            if name == 'index':
                output[name.upper()] = tile_index
            elif name == 'ebmv':
                if tileinfo is None:
                    config = desisurvey.config.Configuration()
                    tileinfo = astropy.table.Table(
                        desimodel.io.load_tiles(onlydesi=True, extra=False,
                        tilesfile=config.tiles_file()))
                    assert np.all(tileinfo['TILEID'] == table['tileid'])
                output[name.upper()] = tileinfo['EBV_MED'][tile_index]
            else:
                if name not in table.colnames or len(table[name].shape) != 1:
                    raise ValueError(
                        'Invalid tile field name: {0}.'.format(name))
                output[name.upper()] = table[name][tile_index]
        for name in exp_fields.split(','):
            name = name.lower()
            if name == 'snr2cum':
                snr2cum = np.cumsum(
                    table['snr2frac'], axis=1).flatten()[order]
                output[name.upper()] = astropy.table.Column(
                    snr2cum, format='%.3f',
                    description='Cummulative fraction of target S/N**2')
            elif name == 'night':
                mjd = table['mjd'].flatten()[order]
                night = np.empty(len(mjd), dtype=(str, 8))
                for i in range(len(mjd)):
                    night[i] = str(desisurvey.utils.get_date(mjd[i])).replace('-', '')
                output[name.upper()] = astropy.table.Column(
                    night,
                    description='Date at start of night when exposure taken')
            elif name == 'lst':
                mjd = table['mjd'].flatten()[order]
                times = astropy.time.Time(
                    mjd, format='mjd', location=desisurvey.utils.get_location())
                lst = times.sidereal_time('apparent').to(u.deg).value
                output[name.upper()] = astropy.table.Column(
                    lst, format='%.1f', unit='deg',
                    description='Apparent local sidereal time in degrees')
            elif name == 'program':
                exppass = table['pass'][tile_index]
                try:
                    from desimodel.footprint import pass2program
                    program = pass2program(exppass)
                except ImportError:
                    #- desimodel < 0.9.1 doesn't have pass2program, so
                    #- hardcode the mapping that it did have
                    program = np.empty(len(exppass), dtype=(str, 6))
                    program[:] = 'BRIGHT'
                    program[exppass < 4] = 'DARK'
                    program[exppass == 4] = 'GRAY'
                    
                proglen = len(max(program, key=len))
                if proglen < 6: # need at least six characters for 'CALIB' program
                    proglen = 6

                output[name.upper()] = astropy.table.Column(program,
                                                            dtype='<U{}'.format(proglen),
                                                            description='Program name')
            elif name == 'flavor':
                flavor = np.empty(len(exppass), dtype=(str, 7))
                flavor[:] = 'science'
                output[name.upper()] = astropy.table.Column(flavor,
                    description='Exposure flavor')
            elif name == 'expid':
                output[name.upper()] = astropy.table.Column(
                    expid[order], description='Exposure index')
            else:
                if name not in table.colnames or len(table[name].shape) != 2:
                    raise ValueError(
                        'Invalid exposure field name: {0}.'.format(name))
                output[name.upper()] = table[name].flatten()[order]

        return output
Exemplo n.º 6
0
    def __init__(self, restore=None, max_exposures=32):

        self.log = desiutil.log.get_logger()

        # Lookup the completeness SNR2 threshold to use.
        config = desisurvey.config.Configuration()
        self.min_snr2 = config.min_snr2_fraction()

        if restore is None:
            # Load the list of tiles to observe.
            tiles = astropy.table.Table(
                desimodel.io.load_tiles(onlydesi=True, extra=False,
                    tilesfile=config.tiles_file() ))
            num_tiles = len(tiles)
            # Initialize a new progress table.
            meta = dict(VERSION=_version)
            table = astropy.table.Table(meta=meta)
            table['tileid'] = astropy.table.Column(
                length=num_tiles, dtype=np.int32,
                description='DESI footprint tile ID')
            table['pass'] = astropy.table.Column(
                length=num_tiles, dtype=np.int32,
                description='Observing pass number starting at zero')
            table['ra'] = astropy.table.Column(
                length=num_tiles, description='TILE center RA in degrees',
                unit='deg', format='%.1f')
            table['dec'] = astropy.table.Column(
                length=num_tiles, description='TILE center DEC in degrees',
                unit='deg', format='%.1f')
            table['status'] = astropy.table.Column(
                length=num_tiles, dtype=np.int32,
                description='Observing status: 0=none, 1=partial, 2=done')
            table['covered'] = astropy.table.Column(
                length=num_tiles, dtype=np.int32,
                description='Tile covered on this day number >=0 (or -1)')
            table['available'] = astropy.table.Column(
                length=num_tiles, dtype=np.int32,
                description='Tile available on this day number >=0 (or -1)')
            table['planned'] = astropy.table.Column(
                length=num_tiles, dtype=np.int32,
                description='Tile first planned on this day number >=0 (or -1)')
            # Add per-exposure columns.
            table['mjd'] = astropy.table.Column(
                length=num_tiles, shape=(max_exposures,), format='%.5f',
                description='MJD of exposure start time')
            table['exptime'] = astropy.table.Column(
                length=num_tiles, shape=(max_exposures,), format='%.1f',
                description='Exposure duration in seconds', unit='s')
            table['snr2frac'] = astropy.table.Column(
                length=num_tiles, shape=(max_exposures,), format='%.3f',
                description='Fraction of target S/N**2 ratio achieved')
            table['airmass'] = astropy.table.Column(
                length=num_tiles, shape=(max_exposures,), format='%.1f',
                description='Estimated airmass of observation')
            table['seeing'] = astropy.table.Column(
                length=num_tiles, shape=(max_exposures,), format='%.1f',
                description='Estimated FWHM seeing of observation in arcsecs',
                unit='arcsec')
            table['transparency'] = astropy.table.Column(
                length=num_tiles, shape=(max_exposures,), format='%.1f',
                description='Estimated transparency of observation')
            table['moonfrac'] = astropy.table.Column(
                length=num_tiles, shape=(max_exposures,), format='%.3f',
                description='Moon illuminated fraction (0-1)')
            table['moonalt'] = astropy.table.Column(
                length=num_tiles, shape=(max_exposures,), format='%.1f',
                description='Moon altitude angle in degrees', unit='deg')
            table['moonsep'] = astropy.table.Column(
                length=num_tiles, shape=(max_exposures,), format='%.1f',
                description='Moon-tile separation angle in degrees', unit='deg')
            # Copy tile data.
            table['tileid'] = tiles['TILEID']
            table['pass'] = tiles['PASS']
            table['ra'] = tiles['RA']
            table['dec'] = tiles['DEC']
            # Initialize other columns.
            table['status'] = 0
            table['covered'] = -1
            table['available'] = -1
            table['planned'] = -1
            table['mjd'] = 0.
            table['exptime'] = 0.
            table['snr2frac'] = 0.
            table['airmass'] = 0.
            table['seeing'] = 0.
            table['transparency'] = 0.

        else:
            if isinstance(restore, Progress):
                table = restore._table
            elif isinstance(restore, astropy.table.Table):
                table = restore
            else:
                filename = config.get_path(restore)
                if not os.path.exists(filename):
                    raise ValueError('Invalid restore: {0}.'.format(restore))
                table = astropy.table.Table.read(filename)
                self.log.info('Loaded progress from {0}.'.format(filename))
            # Check that this table has the current version.
            if table.meta['VERSION'] != _version:
                raise RuntimeError(
                    'Progress table has incompatible version {0}.'
                    .format(table.meta['VERSION']))
            # Check that the status column matches the current min_snr2.
            snr2sum = table['snr2frac'].data.sum(axis=1)
            if not np.all(snr2sum >= 0):
                raise RuntimeError('Found invalid snr2frac values.')
            status = np.ones_like(table['status'])
            status[snr2sum == 0] = 0
            status[snr2sum >= self.min_snr2] = 2
            if not np.all(table['status'] == status):
                self.log.warn('Updating status values for min(SNR2) = {0:.1f}.'
                              .format(self.min_snr2))
                table['status'] = status
            # We could do more sanity checks here, but they shouldn't be
            # necessary unless the table has been modified outside this class.

        # Initialize attributes from table data.
        self._table = table
        mjd = table['mjd'].data
        observed = mjd > 0
        if np.any(observed):
            self._num_exp = np.count_nonzero(observed)
            self._first_mjd = np.min(mjd[observed])
            self._last_mjd = np.max(mjd[observed])
            last = np.argmax(mjd.max(axis=1))
            self._last_tile = self._table[last]
        else:
            self._num_exp = 0
            self._first_mjd = self._last_mjd = 0.
            self._last_tile = None
Exemplo n.º 7
0
def initialize(ephem,
               start_date=None,
               stop_date=None,
               step_size=5.0,
               healpix_nside=16,
               output_name='scheduler.fits'):
    """Calculate exposure-time factors over a grid of times and pointings.

    Takes about 9 minutes to run and writes a 1.3Gb output file with the
    default parameters.

    Requires that healpy is installed.

    Parameters
    ----------
    ephem : desisurvey.ephem.Ephemerides
        Tabulated ephemerides data to use for planning.
    start_date : date or None
        Survey planning starts on the evening of this date. Must be convertible
        to a date using :func:`desisurvey.utils.get_date`.  Use the first night
        of the ephemerides when None.
    stop_date : date or None
        Survey planning stops on the morning of this date. Must be convertible
        to a date using :func:`desisurvey.utils.get_date`.  Use the first night
        of the ephemerides when None.
    step_size : :class:`astropy.units.Quantity`
        Exposure-time factors are tabulated at this interval during each night.
    healpix_nside : int
        Healpix NSIDE parameter to use for binning the sky. Must be a power of
        two.  Values larger than 16 will lead to holes in the footprint with
        the current implementation.
    output_name : str
        Name of the FITS output file where results are saved. A relative path
        refers to the :meth:`configuration output path
        <desisurvey.config.Configuration.get_path>`.
    """
    import healpy

    if not isinstance(step_size, u.Quantity):
        step_size = step_size * u.min
    log = desiutil.log.get_logger()

    # Freeze IERS table for consistent results.
    desisurvey.utils.freeze_iers()

    config = desisurvey.config.Configuration()
    output_name = config.get_path(output_name)

    start_date = desisurvey.utils.get_date(start_date or config.first_day())
    stop_date = desisurvey.utils.get_date(stop_date or config.last_day())
    if start_date >= stop_date:
        raise ValueError('Expected start_date < stop_date.')
    mjd = ephem._table['noon']
    sel = ((mjd >= desisurvey.utils.local_noon_on_date(start_date).mjd) &
           (mjd < desisurvey.utils.local_noon_on_date(stop_date).mjd))
    t = ephem._table[sel]
    num_nights = len(t)

    # Build a grid of elapsed time relative to local midnight during each night.
    midnight = t['noon'] + 0.5
    t_edges = desisurvey.ephem.get_grid(step_size)
    t_centers = 0.5 * (t_edges[1:] + t_edges[:-1])
    num_points = len(t_centers)

    # Create an empty HDU0 with header info.
    header = astropy.io.fits.Header()
    header['START'] = str(start_date)
    header['STOP'] = str(stop_date)
    header['NSIDE'] = healpix_nside
    header['NPOINTS'] = num_points
    header['STEP'] = step_size.to(u.min).value
    hdus = astropy.io.fits.HDUList()
    hdus.append(astropy.io.fits.ImageHDU(header=header))

    # Save time grid.
    hdus.append(astropy.io.fits.ImageHDU(name='GRID', data=t_edges))

    # Load the list of tiles to observe.
    tiles = astropy.table.Table(
        desimodel.io.load_tiles(onlydesi=True,
                                extra=False,
                                tilesfile=config.tiles_file()))

    # Build the footprint as a healpix map of the requested size.
    # The footprint includes any pixel containing at least one tile center.
    npix = healpy.nside2npix(healpix_nside)
    footprint = np.zeros(npix, bool)
    pixels = healpy.ang2pix(healpix_nside, np.radians(90 - tiles['DEC'].data),
                            np.radians(tiles['RA'].data))
    footprint[np.unique(pixels)] = True
    footprint_pixels = np.where(footprint)[0]
    num_footprint = len(footprint_pixels)
    log.info('Footprint contains {0} pixels.'.format(num_footprint))

    # Sort pixels in order of increasing phi + 60deg so that the north and south
    # galactic caps are contiguous in the arrays we create below.
    pix_theta, pix_phi = healpy.pix2ang(healpix_nside, footprint_pixels)
    pix_dphi = np.fmod(pix_phi + np.pi / 3, 2 * np.pi)
    sort_order = np.argsort(pix_dphi)
    footprint_pixels = footprint_pixels[sort_order]
    # Calculate sorted pixel (ra,dec).
    pix_theta, pix_phi = healpy.pix2ang(healpix_nside, footprint_pixels)
    pix_ra, pix_dec = np.degrees(pix_phi), 90 - np.degrees(pix_theta)

    # Record per-tile info needed for planning.
    table = astropy.table.Table()
    table['tileid'] = tiles['TILEID'].astype(np.int32)
    table['ra'] = tiles['RA'].astype(np.float32)
    table['dec'] = tiles['DEC'].astype(np.float32)
    table['EBV'] = tiles['EBV_MED'].astype(np.float32)
    table['pass'] = tiles['PASS'].astype(np.int16)
    # Map each tile ID to the corresponding index in our spatial arrays.
    mapper = np.zeros(npix, int)
    mapper[footprint_pixels] = np.arange(len(footprint_pixels))
    table['map'] = mapper[pixels].astype(np.int16)
    # Use a small int to identify the program, ordered by sky brightness:
    # 1=DARK, 2=GRAY, 3=BRIGHT.
    table['program'] = np.full(len(tiles), 4, np.int16)
    for i, program in enumerate(('DARK', 'GRAY', 'BRIGHT')):
        table['program'][tiles['PROGRAM'] == program] = i + 1
    assert np.all(table['program'] > 0)
    hdu = astropy.io.fits.table_to_hdu(table)
    hdu.name = 'TILES'
    hdus.append(hdu)

    # Average E(B-V) for all tiles falling into a pixel.
    tiles_per_pixel = np.bincount(pixels, minlength=npix)
    EBV = np.bincount(pixels, weights=tiles['EBV_MED'], minlength=npix)
    EBV[footprint] /= tiles_per_pixel[footprint]

    # Calculate dust extinction exposure-time factor.
    f_EBV = 1. / desisurvey.etc.dust_exposure_factor(EBV)

    # Save HDU with the footprint and static dust exposure map.
    table = astropy.table.Table()
    table['pixel'] = footprint_pixels
    table['dust'] = f_EBV[footprint_pixels]
    table['ra'] = pix_ra
    table['dec'] = pix_dec
    hdu = astropy.io.fits.table_to_hdu(table)
    hdu.name = 'STATIC'
    hdus.append(hdu)

    # Prepare a table of calendar data.
    calendar = astropy.table.Table()
    calendar['midnight'] = midnight
    calendar['monsoon'] = np.zeros(num_nights, bool)
    calendar['fullmoon'] = np.zeros(num_nights, bool)
    calendar['weather'] = np.zeros(num_nights, np.float32)
    # Hardcode annualized average weight.
    weather_weights = np.full(num_nights, 0.723)

    # Prepare a table of ephemeris data.
    etable = astropy.table.Table()
    # Program codes ordered by increasing sky brightness:
    # 1=DARK, 2=GRAY, 3=BRIGHT, 4=DAYTIME.
    etable['program'] = np.full(num_nights * num_points, 4, dtype=np.int16)
    etable['moon_frac'] = np.zeros(num_nights * num_points, dtype=np.float32)
    etable['moon_ra'] = np.zeros(num_nights * num_points, dtype=np.float32)
    etable['moon_dec'] = np.zeros(num_nights * num_points, dtype=np.float32)
    etable['moon_alt'] = np.zeros(num_nights * num_points, dtype=np.float32)
    etable['zenith_ra'] = np.zeros(num_nights * num_points, dtype=np.float32)
    etable['zenith_dec'] = np.zeros(num_nights * num_points, dtype=np.float32)

    # Tabulate MJD and apparent LST values for each time step. We don't save
    # MJD values since they are cheap to reconstruct from the index, but
    # do use them below.
    mjd0 = desisurvey.utils.local_noon_on_date(start_date).mjd + 0.5
    mjd = mjd0 + np.arange(num_nights)[:, np.newaxis] + t_centers
    times = astropy.time.Time(mjd,
                              format='mjd',
                              location=desisurvey.utils.get_location())
    etable['lst'] = times.sidereal_time('apparent').flatten().to(u.deg).value

    # Build sky coordinates for each pixel in the footprint.
    pix_theta, pix_phi = healpy.pix2ang(healpix_nside, footprint_pixels)
    pix_ra, pix_dec = np.degrees(pix_phi), 90 - np.degrees(pix_theta)
    pix_sky = astropy.coordinates.ICRS(pix_ra * u.deg, pix_dec * u.deg)

    # Initialize exposure factor calculations.
    alt, az = np.full(num_points, 90.) * u.deg, np.zeros(num_points) * u.deg
    fexp = np.zeros((num_nights * num_points, num_footprint), dtype=np.float32)
    vband_extinction = 0.15154
    one = np.ones((num_points, num_footprint))

    # Loop over nights.
    for i in range(num_nights):
        night = ephem.get_night(midnight[i])
        date = desisurvey.utils.get_date(midnight[i])
        if date.day == 1:
            log.info('Starting {0} (completed {1}/{2} nights)'.format(
                date.strftime('%b %Y'), i, num_nights))
        # Initialize the slice of the fexp[] time index for this night.
        sl = slice(i * num_points, (i + 1) * num_points)
        # Do we expect to observe on this night?
        calendar[i]['monsoon'] = desisurvey.utils.is_monsoon(midnight[i])
        calendar[i]['fullmoon'] = ephem.is_full_moon(midnight[i])
        # Look up expected dome-open fraction due to weather.
        calendar[i]['weather'] = weather_weights[i]
        # Calculate the program during this night (default is 4=DAYTIME).
        mjd = midnight[i] + t_centers
        dark, gray, bright = ephem.tabulate_program(mjd)
        etable['program'][sl][dark] = 1
        etable['program'][sl][gray] = 2
        etable['program'][sl][bright] = 3
        # Zero the exposure factor whenever we are not oberving.
        ##fexp[sl] = (dark | gray | bright)[:, np.newaxis]
        fexp[sl] = 1.
        # Transform the local zenith to (ra,dec).
        zenith = desisurvey.utils.get_observer(
            times[i], alt=alt, az=az).transform_to(astropy.coordinates.ICRS)
        etable['zenith_ra'][sl] = zenith.ra.to(u.deg).value
        etable['zenith_dec'][sl] = zenith.dec.to(u.deg).value
        # Calculate zenith angles to each pixel in the footprint.
        pix_sep = pix_sky.separation(zenith[:, np.newaxis])
        # Zero the exposure factor for pixels below the horizon.
        visible = pix_sep < 90 * u.deg
        fexp[sl][~visible] = 0.
        # Calculate the airmass exposure-time penalty.
        X = desisurvey.utils.cos_zenith_to_airmass(np.cos(pix_sep[visible]))
        fexp[sl][visible] /= desisurvey.etc.airmass_exposure_factor(X)
        # Loop over objects we need to avoid.
        for name in config.avoid_bodies.keys:
            f_obj = desisurvey.ephem.get_object_interpolator(night, name)
            # Calculate this object's (dec,ra) path during the night.
            obj_dec, obj_ra = f_obj(mjd)
            sky_obj = astropy.coordinates.ICRS(
                ra=obj_ra[:, np.newaxis] * u.deg,
                dec=obj_dec[:, np.newaxis] * u.deg)
            # Calculate the separation angles to each pixel in the footprint.
            obj_sep = pix_sky.separation(sky_obj)
            if name == 'moon':
                etable['moon_ra'][sl] = obj_ra
                etable['moon_dec'][sl] = obj_dec
                # Calculate moon altitude during the night.
                moon_alt, _ = desisurvey.ephem.get_object_interpolator(
                    night, 'moon', altaz=True)(mjd)
                etable['moon_alt'][sl] = moon_alt
                moon_zenith = (90 - moon_alt[:, np.newaxis]) * u.deg
                moon_up = moon_alt > 0
                assert np.all(moon_alt[gray] > 0)
                # Calculate the moon illuminated fraction during the night.
                moon_frac = ephem.get_moon_illuminated_fraction(mjd)
                etable['moon_frac'][sl] = moon_frac
                # Convert to temporal moon phase.
                moon_phase = np.arccos(2 * moon_frac[:, np.newaxis] -
                                       1) / np.pi
                # Calculate scattered moon V-band brightness at each pixel.
                V = specsim.atmosphere.krisciunas_schaefer(
                    pix_sep, moon_zenith, obj_sep, moon_phase,
                    desisurvey.etc._vband_extinction).value
                # Estimate the exposure time factor from V.
                X = np.dstack((one, np.exp(-V), 1 / V, 1 / V**2, 1 / V**3))
                T = X.dot(desisurvey.etc._moonCoefficients)
                # No penalty when the moon is below the horizon.
                T[moon_alt < 0, :] = 1.
                fexp[sl] *= 1. / T
                # Veto pointings within avoidance size when the moon is
                # above the horizon. Apply Gaussian smoothing to the veto edge.
                veto = np.ones_like(T)
                dsep = (obj_sep - config.avoid_bodies.moon()).to(u.deg).value
                veto[dsep <= 0] = 0.
                veto[dsep > 0] = 1 - np.exp(-0.5 * (dsep[dsep > 0] / 3)**2)
                veto[moon_alt < 0] = 1.
                fexp[sl] *= veto
            else:
                # Lookup the avoidance size for this object.
                size = getattr(config.avoid_bodies, name)()
                # Penalize the exposure-time with a factor
                # 1 - exp(-0.5*(obj_sep/size)**2)
                penalty = 1. - np.exp(-0.5 * (obj_sep / size).to(1).value**2)
                fexp[sl] *= penalty

    # Save calendar table.
    hdu = astropy.io.fits.table_to_hdu(calendar)
    hdu.name = 'CALENDAR'
    hdus.append(hdu)

    # Save ephemerides table.
    hdu = astropy.io.fits.table_to_hdu(etable)
    hdu.name = 'EPHEM'
    hdus.append(hdu)

    # Save dynamic exposure-time factors.
    hdus.append(astropy.io.fits.ImageHDU(name='DYNAMIC', data=fexp))

    # Finalize the output file.
    try:
        hdus.writeto(output_name, overwrite=True)
    except TypeError:
        # astropy < 1.3 uses the now deprecated clobber.
        hdus.writeto(output_name, clobber=True)
    log.info('Plan initialization saved to {0}'.format(output_name))
Exemplo n.º 8
0
def initialize(ephem, start_date=None, stop_date=None, step_size=5.0,
               healpix_nside=16, output_name='scheduler.fits'):
    """Calculate exposure-time factors over a grid of times and pointings.

    Takes about 9 minutes to run and writes a 1.3Gb output file with the
    default parameters.

    Requires that healpy is installed.

    Parameters
    ----------
    ephem : desisurvey.ephem.Ephemerides
        Tabulated ephemerides data to use for planning.
    start_date : date or None
        Survey planning starts on the evening of this date. Must be convertible
        to a date using :func:`desisurvey.utils.get_date`.  Use the first night
        of the ephemerides when None.
    stop_date : date or None
        Survey planning stops on the morning of this date. Must be convertible
        to a date using :func:`desisurvey.utils.get_date`.  Use the first night
        of the ephemerides when None.
    step_size : :class:`astropy.units.Quantity`
        Exposure-time factors are tabulated at this interval during each night.
    healpix_nside : int
        Healpix NSIDE parameter to use for binning the sky. Must be a power of
        two.  Values larger than 16 will lead to holes in the footprint with
        the current implementation.
    output_name : str
        Name of the FITS output file where results are saved. A relative path
        refers to the :meth:`configuration output path
        <desisurvey.config.Configuration.get_path>`.
    """
    import healpy

    if not isinstance(step_size, u.Quantity):
        step_size = step_size * u.min
    log = desiutil.log.get_logger()

    # Freeze IERS table for consistent results.
    desisurvey.utils.freeze_iers()

    config = desisurvey.config.Configuration()
    output_name = config.get_path(output_name)

    start_date = desisurvey.utils.get_date(start_date or config.first_day())
    stop_date = desisurvey.utils.get_date(stop_date or config.last_day())
    if start_date >= stop_date:
        raise ValueError('Expected start_date < stop_date.')
    mjd = ephem._table['noon']
    sel = ((mjd >= desisurvey.utils.local_noon_on_date(start_date).mjd) &
           (mjd < desisurvey.utils.local_noon_on_date(stop_date).mjd))
    t = ephem._table[sel]
    num_nights = len(t)

    # Build a grid of elapsed time relative to local midnight during each night.
    midnight = t['noon'] + 0.5
    t_edges = desisurvey.ephem.get_grid(step_size)
    t_centers = 0.5 * (t_edges[1:] + t_edges[:-1])
    num_points = len(t_centers)

    # Create an empty HDU0 with header info.
    header = astropy.io.fits.Header()
    header['START'] = str(start_date)
    header['STOP'] = str(stop_date)
    header['NSIDE'] = healpix_nside
    header['NPOINTS'] = num_points
    header['STEP'] = step_size.to(u.min).value
    hdus = astropy.io.fits.HDUList()
    hdus.append(astropy.io.fits.ImageHDU(header=header))

    # Save time grid.
    hdus.append(astropy.io.fits.ImageHDU(name='GRID', data=t_edges))

    # Load the list of tiles to observe.
    tiles = astropy.table.Table(
        desimodel.io.load_tiles(onlydesi=True, extra=False,
            tilesfile=config.tiles_file()))

    # Build the footprint as a healpix map of the requested size.
    # The footprint includes any pixel containing at least one tile center.
    npix = healpy.nside2npix(healpix_nside)
    footprint = np.zeros(npix, bool)
    pixels = healpy.ang2pix(
            healpix_nside, np.radians(90 - tiles['DEC'].data),
            np.radians(tiles['RA'].data))
    footprint[np.unique(pixels)] = True
    footprint_pixels = np.where(footprint)[0]
    num_footprint = len(footprint_pixels)
    log.info('Footprint contains {0} pixels.'.format(num_footprint))

    # Sort pixels in order of increasing phi + 60deg so that the north and south
    # galactic caps are contiguous in the arrays we create below.
    pix_theta, pix_phi = healpy.pix2ang(healpix_nside, footprint_pixels)
    pix_dphi = np.fmod(pix_phi + np.pi / 3, 2 * np.pi)
    sort_order = np.argsort(pix_dphi)
    footprint_pixels = footprint_pixels[sort_order]
    # Calculate sorted pixel (ra,dec).
    pix_theta, pix_phi = healpy.pix2ang(healpix_nside, footprint_pixels)
    pix_ra, pix_dec = np.degrees(pix_phi), 90 - np.degrees(pix_theta)

    # Record per-tile info needed for planning.
    table = astropy.table.Table()
    table['tileid'] = tiles['TILEID'].astype(np.int32)
    table['ra'] = tiles['RA'].astype(np.float32)
    table['dec'] = tiles['DEC'].astype(np.float32)
    table['EBV'] = tiles['EBV_MED'].astype(np.float32)
    table['pass'] = tiles['PASS'].astype(np.int16)
    # Map each tile ID to the corresponding index in our spatial arrays.
    mapper = np.zeros(npix, int)
    mapper[footprint_pixels] = np.arange(len(footprint_pixels))
    table['map'] = mapper[pixels].astype(np.int16)
    # Use a small int to identify the program, ordered by sky brightness:
    # 1=DARK, 2=GRAY, 3=BRIGHT.
    table['program'] = np.full(len(tiles), 4, np.int16)
    for i, program in enumerate(('DARK', 'GRAY', 'BRIGHT')):
        table['program'][tiles['PROGRAM'] == program] = i + 1
    assert np.all(table['program'] > 0)
    hdu = astropy.io.fits.table_to_hdu(table)
    hdu.name = 'TILES'
    hdus.append(hdu)

    # Average E(B-V) for all tiles falling into a pixel.
    tiles_per_pixel = np.bincount(pixels, minlength=npix)
    EBV = np.bincount(pixels, weights=tiles['EBV_MED'], minlength=npix)
    EBV[footprint] /= tiles_per_pixel[footprint]

    # Calculate dust extinction exposure-time factor.
    f_EBV = 1. / desisurvey.etc.dust_exposure_factor(EBV)

    # Save HDU with the footprint and static dust exposure map.
    table = astropy.table.Table()
    table['pixel'] = footprint_pixels
    table['dust'] = f_EBV[footprint_pixels]
    table['ra'] = pix_ra
    table['dec'] = pix_dec
    hdu = astropy.io.fits.table_to_hdu(table)
    hdu.name = 'STATIC'
    hdus.append(hdu)

    # Prepare a table of calendar data.
    calendar = astropy.table.Table()
    calendar['midnight'] = midnight
    calendar['monsoon'] = np.zeros(num_nights, bool)
    calendar['fullmoon'] = np.zeros(num_nights, bool)
    calendar['weather'] = np.zeros(num_nights, np.float32)
    # Hardcode annualized average weight.
    weather_weights = np.full(num_nights, 0.723)

    # Prepare a table of ephemeris data.
    etable = astropy.table.Table()
    # Program codes ordered by increasing sky brightness:
    # 1=DARK, 2=GRAY, 3=BRIGHT, 4=DAYTIME.
    etable['program'] = np.full(num_nights * num_points, 4, dtype=np.int16)
    etable['moon_frac'] = np.zeros(num_nights * num_points, dtype=np.float32)
    etable['moon_ra'] = np.zeros(num_nights * num_points, dtype=np.float32)
    etable['moon_dec'] = np.zeros(num_nights * num_points, dtype=np.float32)
    etable['moon_alt'] = np.zeros(num_nights * num_points, dtype=np.float32)
    etable['zenith_ra'] = np.zeros(num_nights * num_points, dtype=np.float32)
    etable['zenith_dec'] = np.zeros(num_nights * num_points, dtype=np.float32)

    # Tabulate MJD and apparent LST values for each time step. We don't save
    # MJD values since they are cheap to reconstruct from the index, but
    # do use them below.
    mjd0 = desisurvey.utils.local_noon_on_date(start_date).mjd + 0.5
    mjd = mjd0 + np.arange(num_nights)[:, np.newaxis] + t_centers
    times = astropy.time.Time(
        mjd, format='mjd', location=desisurvey.utils.get_location())
    etable['lst'] = times.sidereal_time('apparent').flatten().to(u.deg).value

    # Build sky coordinates for each pixel in the footprint.
    pix_theta, pix_phi = healpy.pix2ang(healpix_nside, footprint_pixels)
    pix_ra, pix_dec = np.degrees(pix_phi), 90 - np.degrees(pix_theta)
    pix_sky = astropy.coordinates.ICRS(pix_ra * u.deg, pix_dec * u.deg)

    # Initialize exposure factor calculations.
    alt, az = np.full(num_points, 90.) * u.deg, np.zeros(num_points) * u.deg
    fexp = np.zeros((num_nights * num_points, num_footprint), dtype=np.float32)
    vband_extinction = 0.15154
    one = np.ones((num_points, num_footprint))

    # Loop over nights.
    for i in range(num_nights):
        night = ephem.get_night(midnight[i])
        date = desisurvey.utils.get_date(midnight[i])
        if date.day == 1:
            log.info('Starting {0} (completed {1}/{2} nights)'
                     .format(date.strftime('%b %Y'), i, num_nights))
        # Initialize the slice of the fexp[] time index for this night.
        sl = slice(i * num_points, (i + 1) * num_points)
        # Do we expect to observe on this night?
        calendar[i]['monsoon'] = desisurvey.utils.is_monsoon(midnight[i])
        calendar[i]['fullmoon'] = ephem.is_full_moon(midnight[i])
        # Look up expected dome-open fraction due to weather.
        calendar[i]['weather'] = weather_weights[i]
        # Calculate the program during this night (default is 4=DAYTIME).
        mjd = midnight[i] + t_centers
        dark, gray, bright = ephem.tabulate_program(mjd)
        etable['program'][sl][dark] = 1
        etable['program'][sl][gray] = 2
        etable['program'][sl][bright] = 3
        # Zero the exposure factor whenever we are not oberving.
        ##fexp[sl] = (dark | gray | bright)[:, np.newaxis]
        fexp[sl] = 1.
        # Transform the local zenith to (ra,dec).
        zenith = desisurvey.utils.get_observer(
            times[i], alt=alt, az=az).transform_to(astropy.coordinates.ICRS)
        etable['zenith_ra'][sl] = zenith.ra.to(u.deg).value
        etable['zenith_dec'][sl] = zenith.dec.to(u.deg).value
        # Calculate zenith angles to each pixel in the footprint.
        pix_sep = pix_sky.separation(zenith[:, np.newaxis])
        # Zero the exposure factor for pixels below the horizon.
        visible = pix_sep < 90 * u.deg
        fexp[sl][~visible] = 0.
        # Calculate the airmass exposure-time penalty.
        X = desisurvey.utils.cos_zenith_to_airmass(np.cos(pix_sep[visible]))
        fexp[sl][visible] /= desisurvey.etc.airmass_exposure_factor(X)
        # Loop over objects we need to avoid.
        for name in config.avoid_bodies.keys:
            f_obj = desisurvey.ephem.get_object_interpolator(night, name)
            # Calculate this object's (dec,ra) path during the night.
            obj_dec, obj_ra = f_obj(mjd)
            sky_obj = astropy.coordinates.ICRS(
                ra=obj_ra[:, np.newaxis] * u.deg,
                dec=obj_dec[:, np.newaxis] * u.deg)
            # Calculate the separation angles to each pixel in the footprint.
            obj_sep = pix_sky.separation(sky_obj)
            if name == 'moon':
                etable['moon_ra'][sl] = obj_ra
                etable['moon_dec'][sl] = obj_dec
                # Calculate moon altitude during the night.
                moon_alt, _ = desisurvey.ephem.get_object_interpolator(
                    night, 'moon', altaz=True)(mjd)
                etable['moon_alt'][sl] = moon_alt
                moon_zenith = (90 - moon_alt[:,np.newaxis]) * u.deg
                moon_up = moon_alt > 0
                assert np.all(moon_alt[gray] > 0)
                # Calculate the moon illuminated fraction during the night.
                moon_frac = ephem.get_moon_illuminated_fraction(mjd)
                etable['moon_frac'][sl] = moon_frac
                # Convert to temporal moon phase.
                moon_phase = np.arccos(2 * moon_frac[:,np.newaxis] - 1) / np.pi
                # Calculate scattered moon V-band brightness at each pixel.
                V = specsim.atmosphere.krisciunas_schaefer(
                    pix_sep, moon_zenith, obj_sep,
                    moon_phase, desisurvey.etc._vband_extinction).value
                # Estimate the exposure time factor from V.
                X = np.dstack((one, np.exp(-V), 1/V, 1/V**2, 1/V**3))
                T = X.dot(desisurvey.etc._moonCoefficients)
                # No penalty when the moon is below the horizon.
                T[moon_alt < 0, :] = 1.
                fexp[sl] *= 1. / T
                # Veto pointings within avoidance size when the moon is
                # above the horizon. Apply Gaussian smoothing to the veto edge.
                veto = np.ones_like(T)
                dsep = (obj_sep - config.avoid_bodies.moon()).to(u.deg).value
                veto[dsep <= 0] = 0.
                veto[dsep > 0] = 1 - np.exp(-0.5 * (dsep[dsep > 0] / 3) ** 2)
                veto[moon_alt < 0] = 1.
                fexp[sl] *= veto
            else:
                # Lookup the avoidance size for this object.
                size = getattr(config.avoid_bodies, name)()
                # Penalize the exposure-time with a factor
                # 1 - exp(-0.5*(obj_sep/size)**2)
                penalty = 1. - np.exp(-0.5 * (obj_sep / size).to(1).value ** 2)
                fexp[sl] *= penalty

    # Save calendar table.
    hdu = astropy.io.fits.table_to_hdu(calendar)
    hdu.name = 'CALENDAR'
    hdus.append(hdu)

    # Save ephemerides table.
    hdu = astropy.io.fits.table_to_hdu(etable)
    hdu.name = 'EPHEM'
    hdus.append(hdu)

    # Save dynamic exposure-time factors.
    hdus.append(astropy.io.fits.ImageHDU(name='DYNAMIC', data=fexp))

    # Finalize the output file.
    try:
        hdus.writeto(output_name, overwrite=True)
    except TypeError:
        # astropy < 1.3 uses the now deprecated clobber.
        hdus.writeto(output_name, clobber=True)
    log.info('Plan initialization saved to {0}'.format(output_name))