Пример #1
0
    def test_end_to_end(self):
        cmd = 'surveyinit --max-cycles 5 --init zero'
        args = desisurvey.scripts.surveyinit.parse(cmd.split()[1:])
        desisurvey.scripts.surveyinit.main(args)
        cmd = 'surveysim --rules rules-layers.yaml --name test'
        args = surveysim.scripts.surveysim.parse(cmd.split()[1:])
        surveysim.scripts.surveysim.main(args)

        config = desisurvey.config.Configuration()
        self.assertTrue(os.path.exists(config.get_path('stats_test.fits')))
        self.assertTrue(os.path.exists(config.get_path('exposures_test.fits')))
        stats = surveysim.stats.SurveyStatistics(restore='stats_test.fits')
        exposures = surveysim.exposures.ExposureList(restore='exposures_test.fits')
Пример #2
0
    def test_end_to_end(self):
        cmd = 'surveyinit --max-cycles 5 --init zero'
        args = desisurvey.scripts.surveyinit.parse(cmd.split()[1:])
        desisurvey.scripts.surveyinit.main(args)
        cmd = 'surveysim --rules rules-layers.yaml --name test'
        args = surveysim.scripts.surveysim.parse(cmd.split()[1:])
        surveysim.scripts.surveysim.main(args)

        config = desisurvey.config.Configuration()
        self.assertTrue(os.path.exists(config.get_path('stats_test.fits')))
        self.assertTrue(os.path.exists(config.get_path('exposures_test.fits')))
        stats = surveysim.stats.SurveyStatistics(restore='stats_test.fits')
        exposures = surveysim.exposures.ExposureList(
            restore='exposures_test.fits')
Пример #3
0
    def save(self, name):
        """Save a snapshot of our current state that can be restored.

        The only internal state required to restore a Scheduler is the array
        of snr2frac values per tile.

        The snapshot file size is about 130Kb.

        Parameters
        ----------
        name : str
            Name of FITS file where the snapshot will be saved. The file will
            be saved under our configuration's output path unless name is
            already an absolute path.  Pass the same name to the constructor's
            ``restore`` argument to restore this snapshot.
        """
        config = desisurvey.config.Configuration()
        fullname = config.get_path(name)
        hdr = astropy.io.fits.Header()
        # Record the last night this scheduler was initialized for.
        hdr['NIGHT'] = self.night.isoformat() if self.night else ''
        # Record the number of completed tiles.
        hdr['NDONE'] = self.completed_by_pass.sum()
        # Save a copy of our snr2frac array.
        astropy.io.fits.PrimaryHDU(self.snr2frac, header=hdr).writeto(fullname, overwrite=True)
        self.log.debug('Saved scheduler snapshot to "{}".'.format(fullname))
Пример #4
0
    def save(self, name):
        """Save a snapshot of our current state that can be restored.

        The only internal state required to restore a Scheduler is the array
        of snr2frac values per tile.

        The snapshot file size is about 130Kb.

        Parameters
        ----------
        name : str
            Name of FITS file where the snapshot will be saved. The file will
            be saved under our configuration's output path unless name is
            already an absolute path.  Pass the same name to the constructor's
            ``restore`` argument to restore this snapshot.
        """
        config = desisurvey.config.Configuration()
        fullname = config.get_path(name)
        hdr = astropy.io.fits.Header()
        # Record the last night this scheduler was initialized for.
        hdr['NIGHT'] = self.night.isoformat() if self.night else ''
        # Record the number of completed tiles.
        hdr['NDONE'] = self.completed_by_pass.sum()
        # Save a copy of our snr2frac array.
        astropy.io.fits.PrimaryHDU(self.snr2frac, header=hdr).writeto(fullname+'.tmp', overwrite=True)
        os.rename(fullname+'.tmp', fullname)
        self.log.debug('Saved scheduler snapshot to "{}".'.format(fullname))
Пример #5
0
    def save(self, name='stats.fits', comment='', overwrite=True):
        """Save a snapshot of these statistics as a binary FITS table.

        The saved file size is ~800 Kb.

        Parameters
        ----------
        name : str
            File name to write. Will be located in the configuration
            output path unless it is an absolute path. Pass the same
            name to the constructor's ``restore`` argument to restore
            this snapshot.
        comment : str
            Comment to include in the saved header, for documentation
            purposes.
        overwrite : bool
            Silently overwrite any existing file when True.
        """
        hdus = astropy.io.fits.HDUList()
        header = astropy.io.fits.Header()
        header['TILES'] = self.tiles.tiles_file
        header['START'] = self.start_date.isoformat()
        header['STOP'] = self.stop_date.isoformat()
        header['COMMENT'] = comment
        header['EXTNAME'] = 'STATS'    
        hdus.append(astropy.io.fits.PrimaryHDU())
        hdus.append(astropy.io.fits.BinTableHDU(self._data, header=header, name='STATS'))
        config = desisurvey.config.Configuration()
        fullname = config.get_path(name)
        hdus.writeto(fullname, overwrite=overwrite)
        log = desiutil.log.get_logger()
        log.info('Saved stats to {}'.format(fullname))
        if comment:
            log.info('Saved with comment "{}".'.format(header['COMMENT']))
Пример #6
0
 def __init__(self,
              start_date=None,
              stop_date=None,
              restore=None,
              tiles_file=None,
              bgs_footprint=None):
     if tiles_file is None:
         self.tiles = desisurvey.tiles.Tiles(bgs_footprint=bgs_footprint)
     else:
         self.tiles = desisurvey.tiles.Tiles(tiles_file=tiles_file,
                                             bgs_footprint=bgs_footprint)
     config = desisurvey.config.Configuration()
     if start_date is None:
         self.start_date = config.first_day()
     else:
         self.start_date = desisurvey.utils.get_date(start_date)
     if stop_date is None:
         self.stop_date = config.last_day()
     else:
         self.stop_date = desisurvey.utils.get_date(stop_date)
     self.num_nights = (self.stop_date - self.start_date).days
     if self.num_nights <= 0:
         raise ValueError('Expected start_date < stop_date.')
     # Build our internal array.
     dtype = []
     for name in 'MJD', 'tsched', :
         dtype.append((name, np.float))
     nprograms = len(self.tiles.PROGRAMS)
     for name in 'topen', 'tdead', :
         dtype.append((name, np.float, (nprograms, )))
     for name in 'tscience', 'tsetup', 'tsplit', :
         dtype.append((name, np.float, (self.tiles.npasses, )))
     for name in 'completed', 'nexp', 'nsetup', 'nsplit', 'nsetup_abort', 'nsplit_abort', :
         dtype.append((name, np.int32, (self.tiles.npasses, )))
     self._data = np.zeros(self.num_nights, dtype)
     if restore is not None:
         # Restore array contents from a FITS file.
         fullname = config.get_path(restore)
         with astropy.io.fits.open(fullname, memmap=None) as hdus:
             header = hdus[1].header
             comment = header['COMMENT']
             if header['TILES'] != self.tiles.tiles_file:
                 raise ValueError('Header mismatch for TILES.')
             if header['START'] != self.start_date.isoformat():
                 raise ValueError('Header mismatch for START.')
             if header['STOP'] != self.stop_date.isoformat():
                 raise ValueError('Header mismatch for STOP.')
             self._data[:] = hdus['STATS'].data
         log = desiutil.log.get_logger()
         log.info('Restored stats from {}'.format(fullname))
         if comment:
             log.info('  Comment: "{}".'.format(comment))
     else:
         # Initialize local-noon MJD timestamp for each night.
         first_noon = desisurvey.utils.local_noon_on_date(
             self.start_date).mjd
         self._data['MJD'] = first_noon + np.arange(self.num_nights)
Пример #7
0
def get_ephem(use_cache=True, write_cache=True):
    """Return tabulated ephemerides for (START_DATE,STOP_DATE).

    The pyephem module must be installed to calculate ephemerides,
    but is not necessary when a FITS file of precalcuated data is
    available.

    Parameters
    ----------
    use_cache : bool
        Use cached ephemerides from memory or disk if possible
        when True.  Otherwise, always calculate from scratch.
    write_cache : bool
        When True, write a generated table so it is available for
        future invocations. Writing only takes place when a
        cached object is not available or ``use_cache`` is False.

    Returns
    -------
    Ephemerides
        Object with tabulated ephemerides for (START_DATE,STOP_DATE).
    """
    global _ephem

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

    # Use standardized string representation of dates.
    start_iso = START_DATE.isoformat()
    stop_iso = STOP_DATE.isoformat()
    range_iso = '({},{})'.format(start_iso, stop_iso)

    log = desiutil.log.get_logger()
    # First check for a cached object in memory.
    if use_cache and _ephem is not None:
        if _ephem.start_date != START_DATE or _ephem.stop_date != STOP_DATE:
            raise RuntimeError('START_DATE, STOP_DATE have changed.')
        log.debug('Returning cached ephemerides for {}.'.format(range_iso))
        return _ephem
    # Next check for a FITS file on disk.
    config = desisurvey.config.Configuration()
    filename = config.get_path('ephem_{}_{}.fits'.format(start_iso, stop_iso))
    if use_cache and os.path.exists(filename):
        # Save restored object in memory.
        _ephem = Ephemerides(START_DATE, STOP_DATE, restore=filename)
        log.info('Restored ephemerides for {} from {}.'
                 .format(range_iso, filename))
        return _ephem
    # Finally, create new ephemerides and save in the memory cache.
    log.info('Building ephemerides for {}...'.format(range_iso))
    _ephem = Ephemerides(START_DATE, STOP_DATE)
    if write_cache:
        # Save the tabulated ephemerides to disk.
        _ephem._table.write(filename, overwrite=True)
        log.info('Saved ephemerides for {} to {}'.format(range_iso, filename))
    return _ephem
Пример #8
0
def get_ephem(use_cache=True, write_cache=True):
    """Return tabulated ephemerides for (START_DATE,STOP_DATE).

    The pyephem module must be installed to calculate ephemerides,
    but is not necessary when a FITS file of precalcuated data is
    available.

    Parameters
    ----------
    use_cache : bool
        Use cached ephemerides from memory or disk if possible
        when True.  Otherwise, always calculate from scratch.
    write_cache : bool
        When True, write a generated table so it is available for
        future invocations. Writing only takes place when a
        cached object is not available or ``use_cache`` is False.

    Returns
    -------
    Ephemerides
        Object with tabulated ephemerides for (START_DATE,STOP_DATE).
    """
    global _ephem

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

    # Use standardized string representation of dates.
    start_iso = START_DATE.isoformat()
    stop_iso = STOP_DATE.isoformat()
    range_iso = '({},{})'.format(start_iso, stop_iso)

    log = desiutil.log.get_logger()
    # First check for a cached object in memory.
    if use_cache and _ephem is not None:
        if _ephem.start_date != START_DATE or _ephem.stop_date != STOP_DATE:
            raise RuntimeError('START_DATE, STOP_DATE have changed.')
        log.debug('Returning cached ephemerides for {}.'.format(range_iso))
        return _ephem
    # Next check for a FITS file on disk.
    config = desisurvey.config.Configuration()
    filename = config.get_path('ephem_{}_{}.fits'.format(start_iso, stop_iso))
    if use_cache and os.path.exists(filename):
        # Save restored object in memory.
        _ephem = Ephemerides(START_DATE, STOP_DATE, restore=filename)
        log.info('Restored ephemerides for {} from {}.'
                 .format(range_iso, filename))
        return _ephem
    # Finally, create new ephemerides and save in the memory cache.
    log.info('Building ephemerides for {}...'.format(range_iso))
    _ephem = Ephemerides(START_DATE, STOP_DATE)
    if write_cache:
        # Save the tabulated ephemerides to disk.
        _ephem._table.write(filename, overwrite=True)
        log.info('Saved ephemerides for {} to {}'.format(range_iso, filename))
    return _ephem
Пример #9
0
def load(name, extra_nexp=0):
    config = desisurvey.config.Configuration()
    name = config.get_path(name)
    with astropy.io.fits.open(name) as hdus:
        header = hdus[0].header
        comment = header['COMMENT']
        nexp = header['NEXP']
        max_nexp = nexp + extra_nexp
        explist = ExposureList(max_nexp=max_nexp)
        explist._exposures[:nexp] = hdus['EXPOSURES'].data
        explist._tiledata[:] = hdus['TILEDATA'].data
    log = desiutil.log.get_logger()
    log.info('Loaded {} exposures from {}'.format(nexp, name))
    if comment:
        log.info('Loaded with comment "{}".'.format(comment))
    return explist
Пример #10
0
 def __init__(self, start_date=None, stop_date=None, restore=None):
     self.tiles = desisurvey.tiles.Tiles()
     config = desisurvey.config.Configuration()
     if start_date is None:
         self.start_date = config.first_day()
     else:
         self.start_date = desisurvey.utils.get_date(start_date)
     if stop_date is None:
         self.stop_date = config.last_day()
     else:
         self.stop_date = desisurvey.utils.get_date(stop_date)
     self.num_nights = (self.stop_date - self.start_date).days
     if self.num_nights <= 0:
         raise ValueError('Expected start_date < stop_date.')
     # Build our internal array.
     dtype = []
     for name in 'MJD', 'tsched',:
         dtype.append((name, np.float))
     nprograms = len(self.tiles.PROGRAMS)
     for name in 'topen', 'tdead',:
         dtype.append((name, np.float, (nprograms,)))
     for name in 'tscience', 'tsetup', 'tsplit',:
         dtype.append((name, np.float, (self.tiles.npasses,)))
     for name in 'completed', 'nexp', 'nsetup', 'nsplit', 'nsetup_abort', 'nsplit_abort',:
         dtype.append((name, np.int32, (self.tiles.npasses,)))
     self._data = np.zeros(self.num_nights, dtype)
     if restore is not None:
         # Restore array contents from a FITS file.
         fullname = config.get_path(restore)
         with astropy.io.fits.open(fullname, memmap=None) as hdus:
             header = hdus[1].header
             comment = header['COMMENT']
             if header['TILES'] != self.tiles.tiles_file:
                 raise ValueError('Header mismatch for TILES.')
             if header['START'] != self.start_date.isoformat():
                 raise ValueError('Header mismatch for START.')
             if header['STOP'] != self.stop_date.isoformat():
                 raise ValueError('Header mismatch for STOP.')
             self._data[:] = hdus['STATS'].data
         log = desiutil.log.get_logger()
         log.info('Restored stats from {}'.format(fullname))
         if comment:
             log.info('  Comment: "{}".'.format(comment))
     else:
         # Initialize local-noon MJD timestamp for each night.
         first_noon = desisurvey.utils.local_noon_on_date(self.start_date).mjd
         self._data['MJD'] = first_noon + np.arange(self.num_nights)
Пример #11
0
 def __init__(self, restore=None, max_nexp=60000,
              existing_exposures=None):
     self.tiles = desisurvey.tiles.get_tiles()
     self._exposures = np.empty(max_nexp, dtype=[
         ('EXPID', np.int32),
         ('MJD', np.float64),
         ('EXPTIME', np.float32),
         ('TILEID', np.int32),
         ('SNR2FRAC', np.float32),
         ('DSNR2FRAC', np.float32),
         ('AIRMASS', np.float32),
         ('SEEING', np.float32),
         ('TRANSP', np.float32),
         ('SKY', np.float32),
     ])
     self._tiledata = np.empty(self.tiles.ntiles, dtype=[
         ('AVAIL', np.int32),
         ('PLANNED', np.int32),
         ('EXPTIME', np.float32),
         ('SNR2FRAC', np.float32),
         ('NEXP', np.int32)
     ])
     if restore is not None:
         config = desisurvey.config.Configuration()
         fullname = config.get_path(restore)
         with astropy.io.fits.open(fullname, memmap=False) as hdus:
             header = hdus[0].header
             comment = header['COMMENT']
             self.nexp = header['NEXP']
             self._exposures[:self.nexp] = hdus['EXPOSURES'].data
             self._tiledata[:] = hdus['TILEDATA'].data
             self.initial_night = desisurvey.utils.get_date(header['INITIAL']) if header['INITIAL'] else None
         log = desiutil.log.get_logger()
         log.info('Restored stats from {}'.format(fullname))
         if comment:
             log.info('  Comment: "{}".'.format(comment))
     else:
         self.nexp = 0
         self._tiledata['AVAIL'] = -1
         self._tiledata['PLANNED'] = -1
         self._tiledata['EXPTIME'] = 0.
         self._tiledata['SNR2FRAC'] = 0.
         self._tiledata['NEXP'] = 0
         self.initial_night = None
     if existing_exposures is not None:
         self.add_existing_exposures(existing_exposures)
Пример #12
0
    def save(self, filename, overwrite=True):
        """Save the generated weather to a file.

        The saved file can be restored using the constructor `restore`
        parameter.

        Parameters
        ----------
        filename : str
            Name of the file where the weather should be saved. A
            relative path name refers to the :meth:`configuration output path
            <desisurvey.config.Configuration.get_path>`.
        overwrite : bool
            Silently overwrite any existing file when this is True.
        """
        config = desisurvey.config.Configuration()
        filename = config.get_path(filename)
        self._table.write(filename, overwrite=overwrite)
        self.log.info('Saved weather to {0}.'.format(filename))
Пример #13
0
    def save(self, filename, overwrite=True):
        """Save the generated weather to a file.

        The saved file can be restored using the constructor `restore`
        parameter.

        Parameters
        ----------
        filename : str
            Name of the file where the weather should be saved. A
            relative path name refers to the :meth:`configuration output path
            <desisurvey.config.Configuration.get_path>`.
        overwrite : bool
            Silently overwrite any existing file when this is True.
        """
        config = desisurvey.config.Configuration()
        filename = config.get_path(filename)
        self._table.write(filename, overwrite=overwrite)
        self.log.info('Saved weather to {0}.'.format(filename))
Пример #14
0
    def save(self, filename, overwrite=True):
        """Save the current progress to a file.

        The saved file can be restored from disk using our constructor,
        although column descriptions will be lost since they are not
        propagated when writing a table to a FITS file.

        Parameters
        ----------
        filename : str
            Name of the file where the progress record should be saved. A
            relative path name refers to the :meth:`configuration output path
            <desisurvey.config.Configuration.get_path>`.
        overwrite : bool
            Silently overwrite any existing file when this is True.
        """
        config = desisurvey.config.Configuration()
        filename = config.get_path(filename)
        self._table.write(filename, overwrite=overwrite)
        self.log.info('Saved progress to {0}.'.format(filename))
Пример #15
0
    def save(self, name='exposures.fits', comment='', overwrite=True):
        """Save exposures to a FITS file with two binary tables.

        The saved file size scales linearly with the number of exposures
        added so far, and is independent of the memory size of this
        object.

        Parameters
        ----------
        name : str
            File name to write. Will be located in the configuration
            output path unless it is an absolute path. Pass the same
            name to the constructor's ``restore`` argument to restore
            this snapshot.
        comment : str
            Comment to include in the saved header, for documentation
            purposes.
        overwrite : bool
            Silently overwrite any existing file when True.
        """
        hdus = astropy.io.fits.HDUList()
        header = astropy.io.fits.Header()
        header['TILES'] = self.tiles.tiles_file
        header['NEXP'] = self.nexp
        header['COMMENT'] = comment
        header['INITIAL'] = self.initial_night.isoformat(
        ) if self.initial_night else ''
        header['EXTNAME'] = 'META'
        hdus.append(astropy.io.fits.PrimaryHDU(header=header))
        hdus.append(
            astropy.io.fits.BinTableHDU(self._exposures[:self.nexp],
                                        name='EXPOSURES'))
        hdus.append(
            astropy.io.fits.BinTableHDU(self._tiledata, name='TILEDATA'))
        config = desisurvey.config.Configuration()
        name = config.get_path(name)
        hdus.writeto(name, overwrite=overwrite)
        log = desiutil.log.get_logger()
        log.info('Saved {} exposures to {}'.format(self.nexp, name))
        if comment:
            log.info('Saved with comment "{}".'.format(header['COMMENT']))
Пример #16
0
    def save(self, name='stats.fits', comment='', overwrite=True):
        """Save a snapshot of these statistics as a binary FITS table.

        The saved file size is ~800 Kb.

        Parameters
        ----------
        name : str
            File name to write. Will be located in the configuration
            output path unless it is an absolute path. Pass the same
            name to the constructor's ``restore`` argument to restore
            this snapshot.
        comment : str
            Comment to include in the saved header, for documentation
            purposes.
        overwrite : bool
            Silently overwrite any existing file when True.
        """
        hdus = astropy.io.fits.HDUList()
        header = astropy.io.fits.Header()
        header['TILES'] = self.tiles.tiles_file
        header['START'] = self.start_date.isoformat()
        header['STOP'] = self.stop_date.isoformat()
        header['COMMENT'] = comment
        header['EXTNAME'] = 'STATS'
        hdus.append(astropy.io.fits.PrimaryHDU())
        hdus.append(
            astropy.io.fits.BinTableHDU(self._data,
                                        header=header,
                                        name='STATS'))
        config = desisurvey.config.Configuration()
        fullname = config.get_path(name)
        hdus.writeto(fullname, overwrite=overwrite)
        log = desiutil.log.get_logger()
        log.info('Saved stats to {}'.format(fullname))
        if comment:
            log.info('Saved with comment "{}".'.format(header['COMMENT']))
Пример #17
0
def main(args):
    """Command-line driver to visualize survey scheduling and progress.
    """
    # Set up the logger
    if args.debug:
        log = desiutil.log.get_logger(desiutil.log.DEBUG)
        args.verbose = True
    elif args.verbose:
        log = desiutil.log.get_logger(desiutil.log.INFO)
    else:
        log = desiutil.log.get_logger(desiutil.log.WARNING)

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

    # Set the output path if requested.
    config = desisurvey.config.Configuration()
    if args.output_path is not None:
        config.set_output_path(args.output_path)
    if args.tiles_file is not None:
        config.tiles_file.set_value(args.tiles_file)

    # Look for the exposures file in the output path by default.
    args.exposures = config.get_path(args.exposures)

    # Initialize.
    animator = Animator(
        args.exposures, args.start, args.stop, args.label, args.scores)
    log.info('Found {0} exposures from {1} to {2} ({3} nights).'
             .format(animator.num_exp, args.start, args.stop,
                     animator.num_nights))
    animator.init_figure(args.nightly)

    if args.expid is not None:
        expid = animator.exposures['EXPID']
        assert np.all(expid == expid[0] + np.arange(len(expid)))
        if (args.expid < expid[0]) or (args.expid > expid[-1]):
            raise RuntimeError('Requested exposure ID {0} not available.'
                               .format(args.expid))
        animator.draw_exposure(args.expid - expid[0], args.nightly)
        save_name = args.save + '.png'
        plt.savefig(save_name)
        log.info('Saved {0}.'.format(save_name))
    else:
        nframes = animator.num_nights if args.nightly else animator.num_exp
        iexp = [0]
        def init():
            return animator.artists
        def update(iframe):
            if (iframe + 1) % args.log_interval == 0:
                log.info('Drawing frame {0}/{1}.'
                         .format(iframe + 1, nframes))
            if args.nightly:
                while not animator.draw_exposure(iexp[0], nightly=True):
                    iexp[0] += 1
            else:
                animator.draw_exposure(iexp=iframe, nightly=False)
            return animator.artists
        log.info('Movie will be {:.1f} mins long at {:.1f} frames/sec.'
                 .format(nframes / (60 * args.fps), args.fps))
        animation = matplotlib.animation.FuncAnimation(
            animator.figure, update, init_func=init, blit=True, frames=nframes)
        writer = matplotlib.animation.writers['ffmpeg'](
            bitrate=2400, fps=args.fps, metadata=dict(artist='surveymovie'))
        save_name = args.save + '.mp4'
        animation.save(save_name, writer=writer, dpi=animator.dpi)
        log.info('Saved {0}.'.format(save_name))
Пример #18
0
    def __init__(self, seed=1, replay='random', time_step=5, restore=None):
        if not isinstance(time_step, u.Quantity):
            time_step = time_step * u.min
        self.log = desiutil.log.get_logger()
        config = desisurvey.config.Configuration()
        ephem = desisurvey.ephem.get_ephem()

        if restore is not None:
            fullname = config.get_path(restore)
            self._table = astropy.table.Table.read(fullname)
            self.start_date = desisurvey.utils.get_date(
                self._table.meta['START'])
            self.stop_date = desisurvey.utils.get_date(
                self._table.meta['STOP'])
            self.num_nights = self._table.meta['NIGHTS']
            self.steps_per_day = self._table.meta['STEPS']
            self.replay = self._table.meta['REPLAY']
            self.log.info('Restored weather from {}.'.format(fullname))
            return
        else:
            self.log.info('Generating random weather with seed={} replay="{}".'
                          .format(seed, replay))

        gen = np.random.RandomState(seed)

        # Use our config to set any unspecified dates.
        start_date = config.first_day()
        stop_date = config.last_day()
        num_nights = (stop_date - start_date).days
        if num_nights <= 0:
            raise ValueError('Expected start_date < stop_date.')

        # Check that the time step evenly divides 24 hours.
        steps_per_day = int(round((1 * u.day / time_step).to(1).value))
        if not np.allclose((steps_per_day * time_step).to(u.day).value, 1.):
            raise ValueError(
                'Requested time_step does not evenly divide 24 hours: {0}.'
                .format(time_step))

        # Calculate the number of times where we will tabulate the weather.
        num_rows = num_nights * steps_per_day
        meta = dict(START=str(start_date), STOP=str(stop_date),
                    NIGHTS=num_nights, STEPS=steps_per_day, REPLAY=replay)
        self._table = astropy.table.Table(meta=meta)

        # Initialize column of MJD timestamps.
        t0 = desisurvey.utils.local_noon_on_date(start_date)
        times = t0 + (np.arange(num_rows) / float(steps_per_day)) * u.day
        self._table['mjd'] = times.mjd

        # Generate a random atmospheric seeing time series.
        dt_sec = 24 * 3600. / steps_per_day
        self._table['seeing'] = desimodel.weather.sample_seeing(
            num_rows, dt_sec=dt_sec, gen=gen).astype(np.float32)

        # Generate a random atmospheric transparency time series.
        self._table['transparency'] = desimodel.weather.sample_transp(
            num_rows, dt_sec=dt_sec, gen=gen).astype(np.float32)

        if replay == 'random':
            # Generate a bootstrap sampling of the historical weather years.
            years_to_simulate = config.last_day().year - config.first_day().year + 1
            history = ['Y{}'.format(year) for year in range(2007, 2018)]
            replay = ','.join(gen.choice(history, years_to_simulate, replace=True))

        # Lookup the dome closed fractions for each night of the survey.
        # This step is deterministic and only depends on the config weather
        # parameter, which specifies which year(s) of historical daily
        # weather to replay during the simulation.
        dome_closed_frac = desimodel.weather.dome_closed_fractions(
            start_date, stop_date, replay=replay)

        # Convert fractions of scheduled time to hours per night.
        ilo, ihi = (start_date - ephem.start_date).days, (stop_date - ephem.start_date).days
        bright_dusk = ephem._table['brightdusk'].data[ilo:ihi]
        bright_dawn = ephem._table['brightdawn'].data[ilo:ihi]
        dome_closed_time = dome_closed_frac * (bright_dawn - bright_dusk)

        # Randomly pick between three scenarios for partially closed nights:
        # 1. closed from dusk, then open the rest of the night.
        # 2. open at dusk, then closed for the rest of the night.
        # 3. open and dusk and dawn, with a closed period during the night.
        # Pick scenarios 1+2 with probability equal to the closed fraction.
        # Use a fixed number of random numbers to decouple from the seeing
        # and transparency sampling below.
        r = gen.uniform(size=num_nights)
        self._table['open'] = np.ones(num_rows, bool)
        for i in range(num_nights):
            sl = slice(i * steps_per_day, (i + 1) * steps_per_day)
            night_mjd = self._table['mjd'][sl]
            # Dome is always closed before dusk and after dawn.
            closed = (night_mjd < bright_dusk[i]) | (night_mjd >= bright_dawn[i])
            if dome_closed_frac[i] == 0:
                # Dome open all night.
                pass
            elif dome_closed_frac[i] == 1:
                # Dome closed all night. This occurs with probability frac / 2.
                closed[:] = True
            elif r[i] < 0.5 * dome_closed_frac[i]:
                # Dome closed during first part of the night.
                # This occurs with probability frac / 2.
                closed |= (night_mjd < bright_dusk[i] + dome_closed_time[i])
            elif r[i] < dome_closed_frac[i]:
                # Dome closed during last part of the night.
                # This occurs with probability frac / 2.
                closed |= (night_mjd > bright_dawn[i] - dome_closed_time[i])
            else:
                # Dome closed during the middle of the night.
                # This occurs with probability 1 - frac.  Use the value of r[i]
                # as the fractional time during the night when the dome reopens.
                dome_open_at = bright_dusk[i] + r[i] * (bright_dawn[i] - bright_dusk[i])
                dome_closed_at = dome_open_at - dome_closed_time[i]
                closed |= (night_mjd >= dome_closed_at) & (night_mjd < dome_open_at)
            self._table['open'][sl][closed] = False

        self.start_date = start_date
        self.stop_date = stop_date
        self.num_nights = num_nights
        self.steps_per_day = steps_per_day
        self.replay = replay
Пример #19
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
Пример #20
0
 def __init__(self, restore=None, design_hourangle=None):
     self.log = desiutil.log.get_logger()
     # Load our configuration.
     config = desisurvey.config.Configuration()
     self.min_snr2frac = config.min_snr2_fraction()
     GRAY = desisurvey.config.Configuration().programs.GRAY
     self.max_prod = GRAY.max_moon_illumination_altitude_product().to(u.deg).value
     self.max_frac = GRAY.max_moon_illumination()
     self.threshold_alt = self.max_prod / self.max_frac
     self.max_airmass = desisurvey.utils.cos_zenith_to_airmass(np.sin(config.min_altitude()))
     # Load static tile info.
     self.tiles = desisurvey.tiles.get_tiles()
     ntiles = self.tiles.ntiles
     # Check hourangles.
     if design_hourangle is None:
         self.design_hourangle = desisurvey.plan.load_design_hourangle()
     else:
         self.design_hourangle = np.asarray(design_hourangle)
     if self.design_hourangle.shape != (self.tiles.ntiles,):
         raise ValueError('Array design_hourangle has wrong shape.')
     # Initialize snr2frac, which is our only internal state.
     if restore is not None:
         # Restore the snr2frac array for a survey in progress.
         fullname = config.get_path(restore)
         if not os.path.exists(fullname):
             raise RuntimeError('Cannot restore scheduler from non-existent "{}".'.format(fullname))
         with astropy.io.fits.open(fullname, memmap=False) as hdus:
             self.snr2frac = hdus[0].data.copy()
         if self.snr2frac.shape != (ntiles,):
             raise ValueError('Invalid snr2frac array shape.')
         self.log.debug('Restored scheduler snapshot from "{}".'.format(fullname))
     else:
         # Initialize for a new survey.
         self.snr2frac = np.zeros(ntiles, float)
     # Initialize arrays derived from snr2frac.
     # Note that indexing of completed_by_pass uses tiles.pass_index, which is not necessarily
     # the same as range(tiles.npasses).
     self.completed = (self.snr2frac >= self.min_snr2frac)
     self.completed_by_pass = np.zeros(self.tiles.npasses, np.int32)
     for passnum in self.tiles.passes:
         idx = self.tiles.pass_index[passnum]
         self.completed_by_pass[idx] = np.count_nonzero(self.completed[self.tiles.passnum == passnum])
     # Allocate memory for internal arrays.
     self.exposure_factor = np.zeros(ntiles)
     self.hourangle = np.zeros(ntiles)
     self.airmass = np.zeros(ntiles)
     self.in_night_pool = np.zeros(ntiles, bool)
     self.tile_sel = np.zeros(ntiles, bool)
     self.LST = 0.
     self.night = None
     # Load the ephemerides to use.
     self.ephem = desisurvey.ephem.get_ephem()
     # Initialize tile availability and priority.
     # No tiles will be scheduled until these are updated using update_tiles().
     self.tile_available = np.zeros(self.tiles.ntiles, bool)
     self.tile_planned = np.zeros(self.tiles.ntiles, bool)
     self.tile_priority = np.zeros(self.tiles.ntiles, float)
     # Lookup avoidance cone angles.
     self.avoid_bodies = {}
     for body in config.avoid_bodies.keys:
         self.avoid_bodies[body] = getattr(config.avoid_bodies, body)().to(u.deg).value
Пример #21
0
    def __init__(self, seed=1, replay='random', time_step=5, restore=None):
        if not isinstance(time_step, u.Quantity):
            time_step = time_step * u.min
        self.log = desiutil.log.get_logger()
        config = desisurvey.config.Configuration()
        ephem = desisurvey.ephem.get_ephem()

        if restore is not None:
            fullname = config.get_path(restore)
            self._table = astropy.table.Table.read(fullname)
            self.start_date = desisurvey.utils.get_date(
                self._table.meta['START'])
            self.stop_date = desisurvey.utils.get_date(
                self._table.meta['STOP'])
            self.num_nights = self._table.meta['NIGHTS']
            self.steps_per_day = self._table.meta['STEPS']
            self.replay = self._table.meta['REPLAY']
            self.log.info('Restored weather from {}.'.format(fullname))
            return
        else:
            self.log.info(
                'Generating random weather with seed={} replay="{}".'.format(
                    seed, replay))

        gen = np.random.RandomState(seed)

        # Use our config to set any unspecified dates.
        start_date = config.first_day()
        stop_date = config.last_day()
        num_nights = (stop_date - start_date).days
        if num_nights <= 0:
            raise ValueError('Expected start_date < stop_date.')

        # Check that the time step evenly divides 24 hours.
        steps_per_day = int(round((1 * u.day / time_step).to(1).value))
        if not np.allclose((steps_per_day * time_step).to(u.day).value, 1.):
            raise ValueError(
                'Requested time_step does not evenly divide 24 hours: {0}.'.
                format(time_step))

        # Calculate the number of times where we will tabulate the weather.
        num_rows = num_nights * steps_per_day
        meta = dict(START=str(start_date),
                    STOP=str(stop_date),
                    NIGHTS=num_nights,
                    STEPS=steps_per_day,
                    REPLAY=replay)
        self._table = astropy.table.Table(meta=meta)

        # Initialize column of MJD timestamps.
        t0 = desisurvey.utils.local_noon_on_date(start_date)
        times = t0 + (np.arange(num_rows) / float(steps_per_day)) * u.day
        self._table['mjd'] = times.mjd

        # Generate a random atmospheric seeing time series.
        dt_sec = 24 * 3600. / steps_per_day
        self._table['seeing'] = desimodel.weather.sample_seeing(
            num_rows, dt_sec=dt_sec, gen=gen).astype(np.float32)

        # Generate a random atmospheric transparency time series.
        self._table['transparency'] = desimodel.weather.sample_transp(
            num_rows, dt_sec=dt_sec, gen=gen).astype(np.float32)

        if replay == 'random':
            # Generate a bootstrap sampling of the historical weather years.
            years_to_simulate = config.last_day().year - config.first_day(
            ).year + 1
            history = ['Y{}'.format(year) for year in range(2007, 2018)]
            replay = ','.join(
                gen.choice(history, years_to_simulate, replace=True))

        # Lookup the dome closed fractions for each night of the survey.
        # This step is deterministic and only depends on the config weather
        # parameter, which specifies which year(s) of historical daily
        # weather to replay during the simulation.
        dome_closed_frac = desimodel.weather.dome_closed_fractions(
            start_date, stop_date, replay=replay)

        # Convert fractions of scheduled time to hours per night.
        ilo, ihi = (start_date -
                    ephem.start_date).days, (stop_date - ephem.start_date).days
        bright_dusk = ephem._table['brightdusk'].data[ilo:ihi]
        bright_dawn = ephem._table['brightdawn'].data[ilo:ihi]
        dome_closed_time = dome_closed_frac * (bright_dawn - bright_dusk)

        # Randomly pick between three scenarios for partially closed nights:
        # 1. closed from dusk, then open the rest of the night.
        # 2. open at dusk, then closed for the rest of the night.
        # 3. open and dusk and dawn, with a closed period during the night.
        # Pick scenarios 1+2 with probability equal to the closed fraction.
        # Use a fixed number of random numbers to decouple from the seeing
        # and transparency sampling below.
        r = gen.uniform(size=num_nights)
        self._table['open'] = np.ones(num_rows, bool)
        for i in range(num_nights):
            sl = slice(i * steps_per_day, (i + 1) * steps_per_day)
            night_mjd = self._table['mjd'][sl]
            # Dome is always closed before dusk and after dawn.
            closed = (night_mjd < bright_dusk[i]) | (night_mjd >=
                                                     bright_dawn[i])
            if dome_closed_frac[i] == 0:
                # Dome open all night.
                pass
            elif dome_closed_frac[i] == 1:
                # Dome closed all night. This occurs with probability frac / 2.
                closed[:] = True
            elif r[i] < 0.5 * dome_closed_frac[i]:
                # Dome closed during first part of the night.
                # This occurs with probability frac / 2.
                closed |= (night_mjd < bright_dusk[i] + dome_closed_time[i])
            elif r[i] < dome_closed_frac[i]:
                # Dome closed during last part of the night.
                # This occurs with probability frac / 2.
                closed |= (night_mjd > bright_dawn[i] - dome_closed_time[i])
            else:
                # Dome closed during the middle of the night.
                # This occurs with probability 1 - frac.  Use the value of r[i]
                # as the fractional time during the night when the dome reopens.
                dome_open_at = bright_dusk[i] + r[i] * (bright_dawn[i] -
                                                        bright_dusk[i])
                dome_closed_at = dome_open_at - dome_closed_time[i]
                closed |= (night_mjd >= dome_closed_at) & (night_mjd <
                                                           dome_open_at)
            self._table['open'][sl][closed] = False

        self.start_date = start_date
        self.stop_date = stop_date
        self.num_nights = num_nights
        self.steps_per_day = steps_per_day
        self.replay = replay
Пример #22
0
def main(args):
    """Command-line driver for updating the survey plan.
    """
    # Check for a valid fa-delay value.
    if args.fa_delay[-1] not in ('d', 'm', 'q'):
        raise ValueError('fa-delay must have the form Nd, Nm or Nq.')
    fa_delay_type = args.fa_delay[-1]
    try:
        fa_delay = int(args.fa_delay[:-1])
    except ValueError:
        raise ValueError('invalid number in fa-delay.')
    if fa_delay < 0:
        raise ValueError('fa-delay value must be >= 0.')

    # Set up the logger
    if args.debug:
        log = desiutil.log.get_logger(desiutil.log.DEBUG)
        args.verbose = True
    elif args.verbose:
        log = desiutil.log.get_logger(desiutil.log.INFO)
    else:
        log = desiutil.log.get_logger(desiutil.log.WARNING)

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

    # Set the output path if requested.
    config = desisurvey.config.Configuration(file_name=args.config_file)
    if args.output_path is not None:
        config.set_output_path(args.output_path)

    # Load ephemerides.
    ephem = desisurvey.ephem.get_ephem()

    # Initialize scheduler.
    if not os.path.exists(config.get_path('scheduler.fits')):
        # Tabulate data used by the scheduler if necessary.
        desisurvey.old.schedule.initialize(ephem)
    scheduler = desisurvey.old.schedule.Scheduler()

    # Read priority rules.
    rules = desisurvey.rules.Rules(args.rules)

    if args.create:
        # Load initial design hour angles for each tile.
        design = astropy.table.Table.read(config.get_path('surveyinit.fits'))
        # Create an empty progress record.
        progress = desisurvey.progress.Progress()
        # Initialize the observing priorities.
        priorities = rules.apply(progress)
        # Create the initial plan.
        plan = desisurvey.plan.create(design['HA'], priorities)
        # Start the survey from scratch.
        start = config.first_day()
    else:
        # Load an existing plan and progress record.
        if not os.path.exists(config.get_path('plan.fits')):
            log.error('No plan.fits found in output path.')
            return -1
        if not os.path.exists(config.get_path('progress.fits')):
            log.error('No progress.fits found in output path.')
            return -1
        plan = astropy.table.Table.read(config.get_path('plan.fits'))
        progress = desisurvey.progress.Progress('progress.fits')
        # Start the new plan from the last observing date.
        with open(config.get_path('last_date.txt'), 'r') as f:
            start = desisurvey.utils.get_date(f.read().rstrip())

    num_complete, num_total, pct = progress.completed(as_tuple=True)

    # Already observed all tiles?
    if num_complete == num_total:
        log.info('All tiles observed!')
        # Return a shell exit code so scripts can detect this condition.
        sys.exit(9)

    # Reached end of the survey?
    if start >= config.last_day():
        log.info('Reached survey end date!')
        # Return a shell exit code so scripts can detect this condition.
        sys.exit(9)

    day_number = desisurvey.utils.day_number(start)
    log.info(
        'Planning night[{0}] {1} with {2:.1f} / {3} ({4:.1f}%) completed.'.
        format(day_number, start, num_complete, num_total, pct))

    bookmarked = False
    if not args.create:

        # Update the priorities for the progress so far.
        new_priority = rules.apply(progress)
        changed_priority = (new_priority != plan['priority'])
        if np.any(changed_priority):
            changed_passes = np.unique(plan['pass'][changed_priority])
            log.info('Priorities changed in pass(es) {0}.'.format(', '.join(
                [str(p) for p in changed_passes])))
            plan['priority'] = new_priority
            bookmarked = True

        # Identify any new tiles that are available for fiber assignment.
        plan = desisurvey.plan.update_available(plan, progress, start, ephem,
                                                fa_delay, fa_delay_type)

        # Will update design HA assignments here...
        pass

    # Update the progress table for the new plan.
    ptable = progress._table
    new_cover = (ptable['covered'] < 0) & (plan['covered'] <= day_number)
    ptable['covered'][new_cover] = day_number
    new_avail = (ptable['available'] < 0) & plan['available']
    ptable['available'][new_avail] = day_number
    new_plan = (ptable['planned'] < 0) & (plan['priority'] > 0)
    ptable['planned'][new_plan] = day_number

    # Save updated progress.
    progress.save('progress.fits')

    # Save the plan.
    plan.write(config.get_path('plan.fits'), overwrite=True)
    if bookmarked:
        # Save a backup of the plan and progress at this point.
        plan.write(config.get_path('plan_{0}.fits'.format(start)))
        progress.save('progress_{0}.fits'.format(start))
Пример #23
0
def main(args):
    """Command-line driver to visualize survey scheduling and progress.
    """
    # Set up the logger
    if args.debug:
        log = desiutil.log.get_logger(desiutil.log.DEBUG)
        args.verbose = True
    elif args.verbose:
        log = desiutil.log.get_logger(desiutil.log.INFO)
    else:
        log = desiutil.log.get_logger(desiutil.log.WARNING)

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

    # Set the output path if requested.
    config = desisurvey.config.Configuration()
    if args.output_path is not None:
        config.set_output_path(args.output_path)
    if args.tiles_file is not None:
        config.tiles_file.set_value(args.tiles_file)

    # Look for the exposures file in the output path by default.
    args.exposures = config.get_path(args.exposures)

    # Initialize.
    animator = Animator(args.exposures, args.start, args.stop, args.label,
                        args.scores)
    log.info('Found {0} exposures from {1} to {2} ({3} nights).'.format(
        animator.num_exp, args.start, args.stop, animator.num_nights))
    animator.init_figure(args.nightly)

    if args.expid is not None:
        expid = animator.exposures['EXPID']
        assert np.all(expid == expid[0] + np.arange(len(expid)))
        if (args.expid < expid[0]) or (args.expid > expid[-1]):
            raise RuntimeError(
                'Requested exposure ID {0} not available.'.format(args.expid))
        animator.draw_exposure(args.expid - expid[0], args.nightly)
        save_name = args.save + '.png'
        plt.savefig(save_name)
        log.info('Saved {0}.'.format(save_name))
    else:
        nframes = animator.num_nights if args.nightly else animator.num_exp
        iexp = [0]

        def init():
            return animator.artists

        def update(iframe):
            if (iframe + 1) % args.log_interval == 0:
                log.info('Drawing frame {0}/{1}.'.format(iframe + 1, nframes))
            if args.nightly:
                while not animator.draw_exposure(iexp[0], nightly=True):
                    iexp[0] += 1
            else:
                animator.draw_exposure(iexp=iframe, nightly=False)
            return animator.artists

        log.info('Movie will be {:.1f} mins long at {:.1f} frames/sec.'.format(
            nframes / (60 * args.fps), args.fps))
        animation = matplotlib.animation.FuncAnimation(animator.figure,
                                                       update,
                                                       init_func=init,
                                                       blit=True,
                                                       frames=nframes)
        writer = matplotlib.animation.writers['ffmpeg'](
            bitrate=2400, fps=args.fps, metadata=dict(artist='surveymovie'))
        save_name = args.save + '.mp4'
        animation.save(save_name, writer=writer, dpi=animator.dpi)
        log.info('Saved {0}.'.format(save_name))
Пример #24
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))
Пример #25
0
    def __init__(self, name='scheduler.fits'):

        self.log = desiutil.log.get_logger()
        config = desisurvey.config.Configuration()
        input_file = config.get_path(name)

        hdus = astropy.io.fits.open(input_file, memmap=False)
        header = hdus[0].header
        self.start_date = desisurvey.utils.get_date(header['START'])
        self.stop_date = desisurvey.utils.get_date(header['STOP'])
        self.num_nights = (self.stop_date - self.start_date).days
        self.nside = header['NSIDE']
        self.step_size = header['STEP'] * u.min
        self.npix = 12 * self.nside**2
        self.pix_area = 360.**2 / np.pi / self.npix * u.deg**2

        self.tiles = astropy.table.Table.read(input_file, hdu='TILES')
        self.tile_coords = astropy.coordinates.ICRS(
            ra=self.tiles['ra'] * u.deg, dec=self.tiles['dec'] * u.deg)

        self.calendar = astropy.table.Table.read(input_file, hdu='CALENDAR')
        self.etable = astropy.table.Table.read(input_file, hdu='EPHEM')

        self.t_edges = hdus['GRID'].data
        self.t_centers = 0.5 * (self.t_edges[1:] + self.t_edges[:-1])
        self.num_times = len(self.t_centers)

        static = astropy.table.Table.read(input_file, hdu='STATIC')
        self.footprint_pixels = static['pixel'].data
        self.footprint = np.zeros(self.npix, bool)
        self.footprint[self.footprint_pixels] = True
        self.footprint_area = len(self.footprint_pixels) * self.pix_area
        self.fdust = static['dust'].data
        self.pixel_ra = static['ra'].data
        self.pixel_dec = static['dec'].data

        self.fexp = hdus['DYNAMIC'].data
        assert self.fexp.shape == (self.num_nights * self.num_times,
                                   len(self.footprint_pixels))

        # Load fallback weights into a (4,3) matrix with row, column
        # indices 0=DARK, 1=GRAY, 2=BRIGHT, 3=DAYTIME. The row index specifies
        # the current program based on the observing time, and the column index
        # specifies the alternate fall back program.  Weights are relative to 1
        # for staying within the nominal program.
        fb = config.fallback_weights
        self.fallback_weights = np.zeros((4, 3))
        self.fallback_weights[:3] = np.identity(3)
        self.fallback_weights[0, 1] = fb.gray_in_dark()
        self.fallback_weights[0, 2] = fb.bright_in_dark()
        self.fallback_weights[1, 0] = fb.dark_in_gray()
        self.fallback_weights[1, 2] = fb.bright_in_gray()
        self.fallback_weights[2, 0] = fb.dark_in_bright()
        self.fallback_weights[2, 1] = fb.gray_in_bright()
        assert np.all(self.fallback_weights >= 0)

        # Calculate target exposure time in seconds of each tile at nominal
        # conditions.
        self.tnom = np.empty(len(self.tiles))
        for i, program in enumerate(('DARK', 'GRAY', 'BRIGHT')):
            sel = self.tiles['program'] == i + 1
            self.tnom[sel] = getattr(config.nominal_exposure_time,
                                     program)().to(u.s).value

        # Initialize calculation of moon, planet positions.
        self.avoid_names = list(config.avoid_bodies.keys)
        self.moon_index = self.avoid_names.index('moon')
        self.avoid_ra = np.empty(len(self.avoid_names))
        self.avoid_dec = np.empty(len(self.avoid_names))
        self.avoid_min = np.empty(len(self.avoid_names))
        for i, name in enumerate(self.avoid_names):
            self.avoid_min[i] = getattr(config.avoid_bodies,
                                        name)().to(u.deg).value
        self.last_date = None
        hdus.close()
Пример #26
0
    def __init__(self, name='scheduler.fits'):

        self.log = desiutil.log.get_logger()
        config = desisurvey.config.Configuration()
        input_file = config.get_path(name)

        hdus = astropy.io.fits.open(input_file, memmap=False)
        header = hdus[0].header
        self.start_date = desisurvey.utils.get_date(header['START'])
        self.stop_date = desisurvey.utils.get_date(header['STOP'])
        self.num_nights = (self.stop_date - self.start_date).days
        self.nside = header['NSIDE']
        self.step_size = header['STEP'] * u.min
        self.npix = 12 * self.nside ** 2
        self.pix_area = 360. ** 2 / np.pi / self.npix * u.deg ** 2

        self.tiles = astropy.table.Table.read(input_file, hdu='TILES')
        self.tile_coords = astropy.coordinates.ICRS(
            ra=self.tiles['ra'] * u.deg, dec=self.tiles['dec'] * u.deg)

        self.calendar = astropy.table.Table.read(input_file, hdu='CALENDAR')
        self.etable = astropy.table.Table.read(input_file, hdu='EPHEM')

        self.t_edges = hdus['GRID'].data
        self.t_centers = 0.5 * (self.t_edges[1:] + self.t_edges[:-1])
        self.num_times = len(self.t_centers)

        static = astropy.table.Table.read(input_file, hdu='STATIC')
        self.footprint_pixels = static['pixel'].data
        self.footprint = np.zeros(self.npix, bool)
        self.footprint[self.footprint_pixels] = True
        self.footprint_area = len(self.footprint_pixels) * self.pix_area
        self.fdust = static['dust'].data
        self.pixel_ra = static['ra'].data
        self.pixel_dec = static['dec'].data

        self.fexp = hdus['DYNAMIC'].data
        assert self.fexp.shape == (
            self.num_nights * self.num_times, len(self.footprint_pixels))

        # Load fallback weights into a (4,3) matrix with row, column
        # indices 0=DARK, 1=GRAY, 2=BRIGHT, 3=DAYTIME. The row index specifies
        # the current program based on the observing time, and the column index
        # specifies the alternate fall back program.  Weights are relative to 1
        # for staying within the nominal program.
        fb = config.fallback_weights
        self.fallback_weights = np.zeros((4, 3))
        self.fallback_weights[:3] = np.identity(3)
        self.fallback_weights[0, 1] = fb.gray_in_dark()
        self.fallback_weights[0, 2] = fb.bright_in_dark()
        self.fallback_weights[1, 0] = fb.dark_in_gray()
        self.fallback_weights[1, 2] = fb.bright_in_gray()
        self.fallback_weights[2, 0] = fb.dark_in_bright()
        self.fallback_weights[2, 1] = fb.gray_in_bright()
        assert np.all(self.fallback_weights >= 0)

        # Calculate target exposure time in seconds of each tile at nominal
        # conditions.
        self.tnom = np.empty(len(self.tiles))
        for i, program in enumerate(('DARK', 'GRAY', 'BRIGHT')):
            sel = self.tiles['program'] == i + 1
            self.tnom[sel] = getattr(
                config.nominal_exposure_time, program)().to(u.s).value

        # Initialize calculation of moon, planet positions.
        self.avoid_names = list(config.avoid_bodies.keys)
        self.moon_index = self.avoid_names.index('moon')
        self.avoid_ra = np.empty(len(self.avoid_names))
        self.avoid_dec = np.empty(len(self.avoid_names))
        self.avoid_min = np.empty(len(self.avoid_names))
        for i, name in enumerate(self.avoid_names):
            self.avoid_min[i] = getattr(
                config.avoid_bodies, name)().to(u.deg).value
        self.last_date = None
        hdus.close()
Пример #27
0
 def __init__(self, restore=None, design_hourangle=None):
     self.log = desiutil.log.get_logger()
     # Load our configuration.
     config = desisurvey.config.Configuration()
     self.min_snr2frac = config.min_snr2_fraction()
     GRAY = desisurvey.config.Configuration().programs.GRAY
     self.max_prod = GRAY.max_moon_illumination_altitude_product().to(u.deg).value
     self.max_frac = GRAY.max_moon_illumination()
     self.threshold_alt = self.max_prod / self.max_frac
     self.max_airmass = desisurvey.utils.cos_zenith_to_airmass(np.sin(config.min_altitude()))
     # Load static tile info.
     self.tiles = desisurvey.tiles.get_tiles()
     ntiles = self.tiles.ntiles
     # Check hourangles.
     if design_hourangle is None:
         self.design_hourangle = desisurvey.plan.load_design_hourangle()
     else:
         self.design_hourangle = np.asarray(design_hourangle)
     if self.design_hourangle.shape != (self.tiles.ntiles,):
         raise ValueError('Array design_hourangle has wrong shape.')
     # Initialize snr2frac, which is our only internal state.
     if restore is not None:
         # Restore the snr2frac array for a survey in progress.
         fullname = config.get_path(restore)
         if not os.path.exists(fullname):
             raise RuntimeError('Cannot restore scheduler from non-existent "{}".'.format(fullname))
         with astropy.io.fits.open(fullname, memmap=False) as hdus:
             self.snr2frac = hdus[0].data.copy()
         if self.snr2frac.shape != (ntiles,):
             raise ValueError('Invalid snr2frac array shape.')
         self.log.debug('Restored scheduler snapshot from "{}".'.format(fullname))
     else:
         # Initialize for a new survey.
         self.snr2frac = np.zeros(ntiles, float)
     # Initialize arrays derived from snr2frac.
     # Note that indexing of completed_by_pass uses tiles.pass_index, which is not necessarily
     # the same as range(tiles.npasses).
     self.completed = (self.snr2frac >= self.min_snr2frac)
     self.completed_by_pass = np.zeros(self.tiles.npasses, np.int32)
     for passnum in self.tiles.passes:
         idx = self.tiles.pass_index[passnum]
         self.completed_by_pass[idx] = np.count_nonzero(self.completed[self.tiles.passnum == passnum])
     # Allocate memory for internal arrays.
     self.exposure_factor = np.zeros(ntiles)
     self.hourangle = np.zeros(ntiles)
     self.airmass = np.zeros(ntiles)
     self.in_night_pool = np.zeros(ntiles, bool)
     self.tile_sel = np.zeros(ntiles, bool)
     self.LST = 0.
     self.night = None
     # Load the ephemerides to use.
     self.ephem = desisurvey.ephem.get_ephem()
     # Initialize tile availability and priority.
     # No tiles will be scheduled until these are updated using update_tiles().
     self.tile_available = np.zeros(self.tiles.ntiles, bool)
     self.tile_planned = np.zeros(self.tiles.ntiles, bool)
     self.tile_priority = np.zeros(self.tiles.ntiles, float)
     # Lookup avoidance cone angles.
     self.avoid_bodies = {}
     for body in config.avoid_bodies.keys:
         self.avoid_bodies[body] = getattr(config.avoid_bodies, body)().to(u.deg).value
Пример #28
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))