Exemplo n.º 1
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)
Exemplo n.º 2
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)
Exemplo n.º 3
0
def day_number(date):
    """Return the number of elapsed days since the start of the survey.

    Does not perform any range check that the date is within the nominal
    survey schedule.

    Parameters
    ----------
    date : astropy.time.Time, datetime.date, datetime.datetime, string or number
        Converted to a date using :func:`get_date`.

    Returns
    -------
    int
        Number of elapsed days since the start of the survey.
    """
    config = desisurvey.config.Configuration()
    return (get_date(date) - config.first_day()).days
Exemplo n.º 4
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
Exemplo n.º 5
0
def initialize(ephem,
               start_date=None,
               stop_date=None,
               step_size=5.0,
               healpix_nside=16,
               output_name='scheduler.fits'):
    """Calculate exposure-time factors over a grid of times and pointings.

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

    Requires that healpy is installed.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    # Finalize the output file.
    try:
        hdus.writeto(output_name, overwrite=True)
    except TypeError:
        # astropy < 1.3 uses the now deprecated clobber.
        hdus.writeto(output_name, clobber=True)
    log.info('Plan initialization saved to {0}'.format(output_name))
Exemplo n.º 6
0
def plot_program(ephem,
                 start_date=None,
                 stop_date=None,
                 style='localtime',
                 include_monsoon=False,
                 include_full_moon=False,
                 include_twilight=True,
                 night_start=-6.5,
                 night_stop=7.5,
                 num_points=500,
                 bg_color='lightblue',
                 save=None):
    """Plot an overview of the DARK/GRAY/BRIGHT program.

    Uses :func:`desisurvey.ephem.get_program_hours` to calculate the
    hours available for each program during each night.

    The matplotlib and basemap packages must be installed to use this function.

    Parameters
    ----------
    ephem : :class:`desisurvey.ephem.Ephemerides`
        Tabulated ephemerides data to use for determining the program.
    start_date : date or None
        First night to include in the plot or use the first date of the
        survey.  Must be convertible to a
        date using :func:`desisurvey.utils.get_date`.
    stop_date : date or None
        First night to include in the plot or use the last date of the
        survey.  Must be convertible to a
        date using :func:`desisurvey.utils.get_date`.
    style : string
        Plot style to use for the vertical axis: "localtime" shows time
        relative to local midnight, "histogram" shows elapsed time for
        each program during each night, and "cumulative" shows the
        cummulative time for each program since ``start_date``.
    include_monsoon : bool
        Include nights during the annual monsoon shutdowns.
    include_fullmoon : bool
        Include nights during the monthly full-moon breaks.
    include_twilight : bool
        Include twilight time at the start and end of each night in
        the BRIGHT program.
    night_start : float
        Start of night in hours relative to local midnight used to set
        y-axis minimum for 'localtime' style and tabulate nightly program.
    night_stop : float
        End of night in hours relative to local midnight used to set
        y-axis maximum for 'localtime' style and tabulate nightly program.
    num_points : int
        Number of subdivisions of the vertical axis to use for tabulating
        the program during each night. The resulting resolution will be
        ``(night_stop - night_start) / num_points`` hours.
    bg_color : matplotlib color
        Axis background color to use.  Must be a valid matplotlib color.
    save : string or None
        Name of file where plot should be saved.  Format is inferred from
        the extension.

    Returns
    -------
    tuple
        Tuple (figure, axes) returned by ``plt.subplots()``.
    """
    import matplotlib.pyplot as plt
    import matplotlib.colors
    import matplotlib.dates
    import matplotlib.ticker
    import pytz

    styles = ('localtime', 'histogram', 'cumulative')
    if style not in styles:
        raise ValueError('Valid styles are {0}.'.format(', '.join(styles)))

    hours = ephem.get_program_hours(start_date, stop_date, include_monsoon,
                                    include_full_moon, include_twilight)
    observing_night = hours.sum(axis=0) > 0

    # Determine plot date range.
    config = desisurvey.config.Configuration()
    if start_date is None:
        start_date = config.first_day()
    else:
        start_date = desisurvey.utils.get_date(start_date)
    if stop_date is None:
        stop_date = config.last_day()
    else:
        stop_date = desisurvey.utils.get_date(stop_date)
    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)

    # Matplotlib date axes uses local time and puts ticks between days
    # at local midnight. We explicitly specify UTC for x-axis labels so
    # that the plot does not depend on the caller's local timezone.
    tz = pytz.utc
    midnight = datetime.time(hour=0)
    xaxis_start = tz.localize(datetime.datetime.combine(start_date, midnight))
    xaxis_stop = tz.localize(datetime.datetime.combine(stop_date, midnight))
    xaxis_lo = matplotlib.dates.date2num(xaxis_start)
    xaxis_hi = matplotlib.dates.date2num(xaxis_stop)

    # Build a grid of elapsed time relative to local midnight during each night.
    midnight = t['noon'] + 0.5
    t_edges = np.linspace(night_start, night_stop, num_points + 1) / 24.
    t_centers = 0.5 * (t_edges[1:] + t_edges[:-1])

    # Initialize the plot.
    fig, ax = plt.subplots(1, 1, figsize=(11, 8.5), squeeze=True)

    if style == 'localtime':

        # Loop over nights to build image data to plot.
        program = np.zeros((num_nights, len(t_centers)))
        for i in np.arange(num_nights):
            if not observing_night[i]:
                continue
            mjd_grid = midnight[i] + t_centers
            dark, gray, bright = ephem.tabulate_program(mjd_grid)
            program[i][dark] = 1.
            program[i][gray] = 2.
            program[i][bright] = 3.

        # Prepare a custom colormap.
        colors = [
            bg_color, program_color['DARK'], program_color['GRAY'],
            program_color['BRIGHT']
        ]
        cmap = matplotlib.colors.ListedColormap(colors, 'programs')

        ax.imshow(program.T,
                  origin='lower',
                  interpolation='none',
                  aspect='auto',
                  cmap=cmap,
                  vmin=-0.5,
                  vmax=+3.5,
                  extent=[xaxis_lo, xaxis_hi, night_start, night_stop])

        # Display 24-hour local time on y axis.
        y_lo = int(np.ceil(night_start))
        y_hi = int(np.floor(night_stop))
        y_ticks = np.arange(y_lo, y_hi + 1, dtype=int)
        y_labels = ['{:02d}h'.format(hr) for hr in (24 + y_ticks) % 24]
        config = desisurvey.config.Configuration()
        ax.set_ylabel('Local Time [{0}]'.format(config.location.timezone()),
                      fontsize='x-large')
        ax.set_yticks(y_ticks)
        ax.set_yticklabels(y_labels)

    else:

        x = xaxis_lo + np.arange(num_nights) + 0.5
        y = hours if style == 'histogram' else np.cumsum(hours, axis=1)
        size = min(15., (300. / num_nights)**2)
        opts = dict(linestyle='-',
                    marker='.' if size > 1 else None,
                    markersize=size)
        ax.plot(x, y[0], color=program_color['DARK'], **opts)
        ax.plot(x, y[1], color=program_color['GRAY'], **opts)
        ax.plot(x, y[2], color=program_color['BRIGHT'], **opts)

        ax.set_facecolor(bg_color)
        ax.set_ylim(0, 1.07 * y.max())
        if style == 'histogram':
            ax.set_ylabel('Hours / Night')
        else:
            ax.set_ylabel('Cumulative Hours')

    # Display dates on the x axis.
    ax.set_xlabel('Survey Date (observing {0} / {1} nights)'.format(
        np.count_nonzero(observing_night), num_nights),
                  fontsize='x-large')
    ax.set_xlim(xaxis_start, xaxis_stop)
    if num_nights < 50:
        # Major ticks at month boundaries.
        ax.xaxis.set_major_locator(matplotlib.dates.MonthLocator(tz=tz))
        ax.xaxis.set_major_formatter(
            matplotlib.dates.DateFormatter('%m/%y', tz=tz))
        # Minor ticks at day boundaries.
        ax.xaxis.set_minor_locator(matplotlib.dates.DayLocator(tz=tz))
    elif num_nights <= 650:
        # Major ticks at month boundaries with no labels.
        ax.xaxis.set_major_locator(matplotlib.dates.MonthLocator(tz=tz))
        ax.xaxis.set_major_formatter(matplotlib.ticker.NullFormatter())
        # Minor ticks at month midpoints with centered labels.
        ax.xaxis.set_minor_locator(
            matplotlib.dates.MonthLocator(bymonthday=15, tz=tz))
        ax.xaxis.set_minor_formatter(
            matplotlib.dates.DateFormatter('%m/%y', tz=tz))
        for tick in ax.xaxis.get_minor_ticks():
            tick.tick1line.set_markersize(0)
            tick.tick2line.set_markersize(0)
            tick.label1.set_horizontalalignment('center')
    else:
        # Major ticks at year boundaries.
        ax.xaxis.set_major_locator(matplotlib.dates.YearLocator(tz=tz))
        ax.xaxis.set_major_formatter(
            matplotlib.dates.DateFormatter('%Y', tz=tz))

    ax.grid(b=True, which='major', color='w', linestyle=':', lw=1)

    # Draw program labels.
    y = 0.975
    opts = dict(fontsize='xx-large',
                fontweight='bold',
                xy=(0, 0),
                horizontalalignment='center',
                verticalalignment='top',
                xycoords='axes fraction',
                textcoords='axes fraction')
    ax.annotate('DARK {0:.1f}h'.format(hours[0].sum()),
                xytext=(0.2, y),
                color=program_color['DARK'],
                **opts)
    ax.annotate('GRAY {0:.1f}h'.format(hours[1].sum()),
                xytext=(0.5, y),
                color=program_color['GRAY'],
                **opts)
    ax.annotate('BRIGHT {0:.1f}h'.format(hours[2].sum()),
                xytext=(0.8, y),
                color=program_color['BRIGHT'],
                **opts)

    plt.tight_layout()
    if save:
        plt.savefig(save)

    return fig, ax
Exemplo n.º 7
0
def plot_iers(which='auto', num_points=500, save=None):
    """Plot IERS data from 2015-2025.

    Plots the UT1-UTC time offset and polar x,y motions over a 10-year
    period that includes the DESI survey, to demonstrate the time ranges
    when different sources (IERS-A, IERS-B) are used and where values
    are predicted then fixed.

    This function is primarily intended to document and debug the
    :func:`desisurvey.utils.update_iers` and :func:`desisurvey.utils.freeze_iers` functions.

    Requires that the matplotlib and basemap packages are installed.

    Parameters
    ----------
    which : 'auto', 'A', 'B' or 'frozen'
        Select which IERS table source to use.  The default 'auto' matches
        the internal astropy default.  Use 'A' or 'B' to force the source
        to be either the latest IERS-A table (which will be downloaded),
        or the IERS-B table packaged with the current version of astropy.
        The 'frozen' option calls :func:`desisurvey.utils.freeze_iers`.
    num_points : int
        The number of times covering 2015-25 to calculate and plot.
    save : string or None
        Name of file where plot should be saved.  Format is inferred from
        the extension.

    Returns
    -------
    tuple
        Tuple (figure, axes) returned by ``plt.subplots()``.
    """
    import matplotlib.pyplot as plt
    import matplotlib.dates
    import matplotlib.ticker

    config = desisurvey.config.Configuration()

    # Calculate UTC midnight timestamps covering 2015 - 2025
    start = astropy.time.Time('2015-01-01 00:00')
    stop = astropy.time.Time('2025-01-01 00:00')
    mjd = np.linspace(start.mjd, stop.mjd, num_points)
    times = astropy.time.Time(mjd, format='mjd')

    t_lo = matplotlib.dates.date2num(start.datetime)
    t_hi = matplotlib.dates.date2num(stop.datetime)
    t = np.linspace(t_lo, t_hi, num_points)

    t_start = matplotlib.dates.date2num(config.first_day())
    t_stop = matplotlib.dates.date2num(config.last_day())

    # Load the specified IERS table.
    if which == 'auto':
        iers = astropy.utils.iers.IERS_Auto.open()
    elif which == 'B':
        iers = astropy.utils.iers.IERS_B.open()
    elif which == 'A':
        # This requires network access to download the latest file.
        iers = astropy.utils.iers.IERS_A.open(astropy.utils.iers.IERS_A_URL)
    elif which == 'frozen':
        desisurvey.utils.freeze_iers()
        iers = astropy.utils.iers.IERS_Auto.open()
    else:
        raise ValueError('Invalid which option.')

    # Calculate UT1-UTC using the IERS table.
    dt, dt_status = iers.ut1_utc(times, return_status=True)
    dt = dt.to(u.s).value

    # Calculate polar x,y motion using the IERS table.
    pmx, pmy, pm_status = iers.pm_xy(times, return_status=True)
    pmx = pmx.to(u.arcsec).value
    pmy = pmy.to(u.arcsec).value

    assert np.all(dt_status == pm_status)
    codes = np.unique(dt_status)

    fig, ax = plt.subplots(2, 1, sharex=True, figsize=(8, 6))

    labels = {-2: 'Fixed', 0: 'IERS-B', 1: 'IERS-A', 2: 'Predict'}
    styles = {-2: 'r:', 0: 'b-', 1: 'go', 2: 'r--'}
    ms = 3
    for code in codes:
        sel = dt_status == code
        ax[0].plot(t[sel], dt[sel], styles[code], ms=ms, label=labels[code])
        ax[1].plot(t[sel], pmx[sel], styles[code], ms=ms, label='Polar x')
        ax[1].plot(t[sel], pmy[sel], styles[code], ms=ms, label='Polar y')

    ax[0].legend(ncol=4)
    ax[0].set_ylabel('UT1 - UTC [s]')
    ax[1].set_ylabel('Polar x,y motion [arcsec]')

    for i in range(2):
        # Vertical lines showing the survey start / stop dates.
        ax[i].axvline(t_start, ls='--', c='k')
        ax[i].axvline(t_stop, ls='--', c='k')

    # Use year labels for the horizontal axis.
    xaxis = ax[1].xaxis
    xaxis.set_major_locator(matplotlib.dates.YearLocator())
    xaxis.set_major_formatter(matplotlib.dates.DateFormatter('%Y'))
    ax[i].set_xlim(start.datetime, stop.datetime)

    plt.tight_layout()
    if save:
        plt.savefig(save)

    return fig, ax
Exemplo n.º 8
0
    def __init__(self, program, lst_edges, lst_hist, subset=None, start=None, stop=None,
                 init='flat', initial_ha=None, stretch=1.0, smoothing_radius=10,
                 center=None, seed=123, weights=[5, 4, 3, 2, 1]):

        tiles = desisurvey.tiles.get_tiles()
        if program not in tiles.PROGRAMS:
            raise ValueError('Invalid program name: "{}".'.format(program))
        if not isinstance(smoothing_radius, u.Quantity):
            smoothing_radius = smoothing_radius * u.deg
        self.log = desiutil.log.get_logger()
        config = desisurvey.config.Configuration()
        self.gen = np.random.RandomState(seed)
        self.cum_weights = np.asarray(weights, float).cumsum()
        self.cum_weights /= self.cum_weights[-1]
        self.stretch = stretch

        if start is None:
            start = config.first_day()
        else:
            start = desisurvey.utils.get_date(start)
        if stop is None:
            stop = config.last_day()
        else:
            stop = desisurvey.utils.get_date(stop)
        if start >= stop:
            raise ValueError('Expected start < stop.')

        self.lst_hist = np.asarray(lst_hist)
        self.lst_hist_sum = self.lst_hist.sum()
        self.nbins = len(lst_hist)
        self.lst_edges = np.asarray(lst_edges)
        if self.lst_edges.shape != (self.nbins + 1,):
            raise ValueError('Inconsistent lengths of lst_hist and lst_edges.')
        self.lst_centers = 0.5 * (self.lst_edges[1:] + self.lst_edges[:-1])
        self.origin = self.lst_edges[0]
        self.binsize = 360. / self.nbins

        # Get nominal exposure time for this program,
        # converted to LST equivalent in degrees.
        texp_nom = getattr(config.nominal_exposure_time, program)()
        self.dlst_nom = 360 * texp_nom.to(u.day).value / 0.99726956583

        # Select the tiles to plan.
        if subset is not None:
            idx = tiles.index(subset)
            # Check that all tiles in the subset belong to program.
            passes = np.unique(tiles.passnum[idx])
            if np.any(tiles.pass_program[passes] != program):
                raise ValueError('Subset contains non-{} tiles.'.format(program))
            tile_sel = np.zeros(tiles.ntiles, bool)
            tile_sel[idx] = True
        else:
            # Use all tiles in the program by default.
            tile_sel = tiles.program_mask[program]

        # Save arrays for the tiles to plan.
        self.ra = wrap(tiles.tileRA[tile_sel], self.origin)
        self.dec = tiles.tileDEC[tile_sel]
        self.tid = tiles.tileID[tile_sel]
        self.idx = np.where(tile_sel)[0]
        self.ntiles = len(self.ra)

        # Calculate the maximum |HA| in degrees allowed for each tile to stay
        # above the survey minimum altitude (plus a 5 deg padding).
        cosZ_min = np.cos(90 * u.deg - (config.min_altitude() + 5 * u.deg))
        latitude = config.location.latitude()
        cosHA_min = (
            (cosZ_min - np.sin(self.dec * u.deg) * np.sin(latitude)) /
            (np.cos(self.dec * u.deg) * np.cos(latitude))).value
        self.max_abs_ha = np.degrees(np.arccos(cosHA_min))

        # Lookup static dust exposure factors for each tile.
        self.dust_factor = tiles.dust_factor[tile_sel]

        # Initialize smoothing weights.
        self.init_smoothing(smoothing_radius)

        self.log.info(
            '{0}: {1:.1f}h for {2} tiles (texp_nom {3:.1f}).'
            .format(program, self.lst_hist_sum, self.ntiles, texp_nom))

        # Initialize metric histories.
        self.scale_history = []
        self.loss_history = []
        self.RMSE_history = []

        # Initialize improve() counters.
        self.nslow = 0
        self.nimprove = 0
        self.nsmooth = 0

        # Calculate schedule plan with HA=0 asignments to establish
        # the smallest possible total exposure time.
        self.plan_tiles = self.get_plan(np.zeros(self.ntiles))
        self.use_plan(save_history=False)
        self.min_total_time = self.plan_hist.sum()

        # Initialize HA assignments for each tile.
        if init == 'zero':
            self.ha = np.zeros(self.ntiles)
        elif init == 'array':
            if subset is None:
                raise ValueError('Must specify subset when init is "array".')
            if len(initial_ha) != self.ntiles:
                raise ValueError('Array initial_ha has wrong length.')
            self.ha = np.asarray(initial_ha)
        elif init == 'flat':
            if center is None:
                # Try 5 equally spaced centers.
                icenters = np.arange(0, self.nbins, self.nbins // 5)
            else:
                # Find the closest bin edge to the requested value.
                icenters = [np.argmin(np.abs(center - lst_edges))]
            min_score = np.inf
            scores = []
            for icenter in icenters:
                # Rotate the available LST histogram to this new center.
                hist = np.roll(self.lst_hist, -icenter)
                center = self.lst_edges[icenter]
                edges = np.linspace(center, center + 360, self.nbins + 1)
                # Calculate the CDF of available LST.
                lst_cdf = np.zeros_like(edges)
                lst_cdf[1:] = np.cumsum(hist)
                lst_cdf /= lst_cdf[-1]
                # Calculate the CDF of planned LST usage relative to the same
                # central LST, assuming HA=0. Instead of spreading each exposure
                # over multiple LST bins, add its entire HA=0 exposure time at
                # LST=RA.
                exptime, _ = self.get_exptime(ha=np.zeros(self.ntiles))
                tile_ra = wrap(tiles.tileRA[tile_sel], center)
                sort_idx = np.argsort(tile_ra)
                tile_cdf = np.cumsum(exptime[sort_idx])
                tile_cdf /= tile_cdf[-1]
                # Use linear interpolation to find an LST for each tile that
                # matches the plan CDF to the available LST CDF.
                new_lst = np.interp(tile_cdf, lst_cdf, edges)
                # Calculate each tile's HA as the difference between its HA=0
                # LST and its new LST after CDF matching.
                ha = np.empty(self.ntiles)
                ha[sort_idx] = np.fmod(new_lst - tile_ra[sort_idx], 360)
                # Clip tiles to their airmass limits.
                ha = np.clip(ha, -self.max_abs_ha, +self.max_abs_ha)
                # Calculate the score for this HA assignment.
                self.plan_tiles = self.get_plan(ha)
                self.use_plan(save_history=False)
                scores.append(self.eval_score(self.plan_hist))
                # Keep track of the best score found so far.
                if scores[-1] < min_score:
                    self.ha = ha.copy()
                    min_score = scores[-1]
                    center_best = center
            self.log.info(
                'Center flat initial HA assignments at LST {:.0f} deg.'
                .format(center_best))
        else:
            raise ValueError('Invalid init option: {0}.'.format(init))

        # Check initial HA assignments against airmass limits.
        ha_clipped = np.clip(self.ha, -self.max_abs_ha, +self.max_abs_ha)
        if not np.all(self.ha == ha_clipped):
            delta = np.abs(self.ha - ha_clipped)
            idx = np.argmax(delta)
            self.log.warn('Clipped {0} HA assignments to airmass limits.'
                          .format(np.count_nonzero(delta)))
            self.log.warn('Max clip is {0:.1f} deg for tile {1}.'
                          .format(delta[idx], self.tid[idx]))
            self.ha = ha_clipped

        # Calculate schedule plan with initial HA asignments.
        self.plan_tiles = self.get_plan(self.ha)
        self.use_plan()
        self.ha_initial = self.ha.copy()
        self.num_adjustments = np.zeros(self.ntiles, int)
Exemplo n.º 9
0
    def __init__(self, start_date=None, stop_date=None, use_twilight=False,
                 weather=None, design_hourangle=None):
        config = desisurvey.config.Configuration()
        if start_date is None:
            start_date = config.first_day()
        else:
            start_date = desisurvey.utils.get_date(start_date)
        if stop_date is None:
            stop_date = config.last_day()
        else:
            stop_date = desisurvey.utils.get_date(stop_date)
        self.num_nights = (stop_date - start_date).days
        if self.num_nights <= 0:
            raise ValueError('Expected start_date < stop_date.')

        self.use_twilight = use_twilight
        # Look up the tiles to observe.
        tiles = desisurvey.tiles.get_tiles()
        self.tiles = tiles
        if design_hourangle is None:
            self.design_hourangle = np.zeros(tiles.ntiles)
        else:
            if len(design_hourangle) != tiles.ntiles:
                raise ValueError('Array design_hourangle has wrong length.')
            self.design_hourangle = np.asarray(design_hourangle)
        # Get weather factors.
        if weather is None:
            self.weather = desisurvey.plan.load_weather(start_date, stop_date)
        else:
            self.weather = np.asarray(weather)
        if self.weather.shape != (self.num_nights,):
            raise ValueError('Array weather has wrong shape.')
        # Get the design hour angles.
        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 != (tiles.ntiles,):
            raise ValueError('Array design_hourangle has wrong shape.')
        # Compute airmass at design hour angles.
        self.airmass = tiles.airmass(self.design_hourangle)
        airmass_factor = desisurvey.etc.airmass_exposure_factor(self.airmass)
        # Load ephemerides.
        ephem = desisurvey.ephem.get_ephem()
        # Compute the expected available and scheduled hours per program.
        scheduled = ephem.get_program_hours(include_twilight=use_twilight)
        available = scheduled * self.weather
        self.cummulative_days = np.cumsum(available, axis=1) / 24.
        # Calculate program parameters.
        ntiles, tsched, openfrac, dust, airmass, nominal = [], [], [], [], [], []
        for program in tiles.PROGRAMS:
            tile_sel = tiles.program_mask[program]
            ntiles.append(np.count_nonzero(tile_sel))
            progindx = tiles.PROGRAM_INDEX[program]
            scheduled_sum = scheduled[progindx].sum()
            tsched.append(scheduled_sum)
            openfrac.append(available[progindx].sum() / scheduled_sum)
            dust.append(tiles.dust_factor[tile_sel].mean())
            airmass.append(airmass_factor[tile_sel].mean())
            nominal.append(getattr(config.nominal_exposure_time, program)().to(u.s).value)
        # Build a table of all forecasting parameters.
        df = collections.OrderedDict()
        self.df = df
        df['Number of tiles'] = np.array(ntiles)
        df['Scheduled time (hr)'] = np.array(tsched)
        df['Dome open fraction'] = np.array(openfrac)
        self.set_overheads()
        df['Nominal exposure (s)'] = np.array(nominal)
        df['Dust factor'] = np.array(dust)
        df['Airmass factor'] = np.array(airmass)
        self.set_factors()
Exemplo n.º 10
0
def plot_iers(which='auto', num_points=500, save=None):
    """Plot IERS data from 2015-2025.

    Plots the UT1-UTC time offset and polar x,y motions over a 10-year
    period that includes the DESI survey, to demonstrate the time ranges
    when different sources (IERS-A, IERS-B) are used and where values
    are predicted then fixed.

    This function is primarily intended to document and debug the
    :func:`desisurvey.utils.update_iers` and :func:`desisurvey.utils.freeze_iers` functions.

    Requires that the matplotlib and basemap packages are installed.

    Parameters
    ----------
    which : 'auto', 'A', 'B' or 'frozen'
        Select which IERS table source to use.  The default 'auto' matches
        the internal astropy default.  Use 'A' or 'B' to force the source
        to be either the latest IERS-A table (which will be downloaded),
        or the IERS-B table packaged with the current version of astropy.
        The 'frozen' option calls :func:`desisurvey.utils.freeze_iers`.
    num_points : int
        The number of times covering 2015-25 to calculate and plot.
    save : string or None
        Name of file where plot should be saved.  Format is inferred from
        the extension.

    Returns
    -------
    tuple
        Tuple (figure, axes) returned by ``plt.subplots()``.
    """
    import matplotlib.pyplot as plt
    import matplotlib.dates
    import matplotlib.ticker

    config = desisurvey.config.Configuration()

    # Calculate UTC midnight timestamps covering 2015 - 2025
    start = astropy.time.Time('2015-01-01 00:00')
    stop = astropy.time.Time('2025-01-01 00:00')
    mjd = np.linspace(start.mjd, stop.mjd, num_points)
    times = astropy.time.Time(mjd, format='mjd')

    t_lo = matplotlib.dates.date2num(start.datetime)
    t_hi = matplotlib.dates.date2num(stop.datetime)
    t = np.linspace(t_lo, t_hi, num_points)

    t_start = matplotlib.dates.date2num(config.first_day())
    t_stop = matplotlib.dates.date2num(config.last_day())

    # Load the specified IERS table.
    if which == 'auto':
        iers = astropy.utils.iers.IERS_Auto.open()
    elif which == 'B':
        iers = astropy.utils.iers.IERS_B.open()
    elif which == 'A':
        # This requires network access to download the latest file.
        iers = astropy.utils.iers.IERS_A.open(astropy.utils.iers.IERS_A_URL)
    elif which == 'frozen':
        desisurvey.utils.freeze_iers()
        iers = astropy.utils.iers.IERS_Auto.open()
    else:
        raise ValueError('Invalid which option.')

    # Calculate UT1-UTC using the IERS table.
    dt, dt_status = iers.ut1_utc(times, return_status=True)
    dt = dt.to(u.s).value

    # Calculate polar x,y motion using the IERS table.
    pmx, pmy, pm_status = iers.pm_xy(times, return_status=True)
    pmx = pmx.to(u.arcsec).value
    pmy = pmy.to(u.arcsec).value

    assert np.all(dt_status == pm_status)
    codes = np.unique(dt_status)

    fig, ax = plt.subplots(2, 1, sharex=True, figsize=(8, 6))

    labels = {-2: 'Fixed', 0: 'IERS-B', 1: 'IERS-A', 2: 'Predict'}
    styles = {-2: 'r:', 0: 'b-', 1: 'go', 2: 'r--'}
    ms = 3
    for code in codes:
        sel = dt_status == code
        ax[0].plot(t[sel], dt[sel], styles[code], ms=ms, label=labels[code])
        ax[1].plot(t[sel], pmx[sel], styles[code], ms=ms, label='Polar x')
        ax[1].plot(t[sel], pmy[sel], styles[code], ms=ms, label='Polar y')

    ax[0].legend(ncol=4)
    ax[0].set_ylabel('UT1 - UTC [s]')
    ax[1].set_ylabel('Polar x,y motion [arcsec]')

    for i in range(2):
        # Vertical lines showing the survey start / stop dates.
        ax[i].axvline(t_start, ls='--', c='k')
        ax[i].axvline(t_stop, ls='--', c='k')

    # Use year labels for the horizontal axis.
    xaxis = ax[1].xaxis
    xaxis.set_major_locator(matplotlib.dates.YearLocator())
    xaxis.set_major_formatter(matplotlib.dates.DateFormatter('%Y'))
    ax[i].set_xlim(start.datetime, stop.datetime)

    plt.tight_layout()
    if save:
        plt.savefig(save)

    return fig, ax
Exemplo n.º 11
0
def parse(options=None):
    """Parse command-line options for running survey simulations.
    """
    parser = argparse.ArgumentParser(
        formatter_class=argparse.ArgumentDefaultsHelpFormatter)
    parser.add_argument('--verbose', action='store_true',
        help='display log messages with severity >= info')
    parser.add_argument('--debug', action='store_true',
        help='display log messages with severity >= debug (implies verbose)')
    parser.add_argument('--log-interval', type=int, default=100, metavar='N',
        help='nightly interval for logging periodic info messages')
    parser.add_argument(
        '--start', type=str, default=None, metavar='DATE',
        help='survey starts on the evening of this day, formatted as YYYY-MM-DD')
    parser.add_argument(
        '--stop', type=str, default=None, metavar='DATE',
        help='survey stops on the morning of this day, formatted as YYYY-MM-DD')
    parser.add_argument(
        '--name', type=str, default='surveysim', metavar='NAME',
        help='name to use for saving simulated stats and exposures')
    parser.add_argument(
        '--comment', type=str, default='', metavar='COMMENT',
        help='comment to save with simulated stats and exposures')
    parser.add_argument(
        '--rules', type=str, default='rules.yaml', metavar='YAML',
        help='name of YAML file with survey strategy rules to use')
    parser.add_argument('--twilight', action='store_true',
        help='include twilight in the scheduled time')
    parser.add_argument('--save-restore', action='store_true',
        help='save/restore the planner and scheduler state after each night')
    parser.add_argument(
        '--seed', type=int, default=1, metavar='N',
        help='random number seed for generating random observing conditions')
    parser.add_argument(
        '--replay', type=str, default='random', metavar='REPLAY',
        help='Replay specific weather years, e.g., "Y2015,Y2011" or "random"')
    parser.add_argument(
        '--output-path', default=None, metavar='PATH',
        help='output path to use instead of config.output_path')
    parser.add_argument(
        '--tiles-file', default=None, metavar='TILES',
        help='name of tiles file to use instead of config.tiles_file')
    parser.add_argument(
        '--config-file', default='config.yaml', metavar='CONFIG',
        help='input configuration file')

    if options is None:
        args = parser.parse_args()
    else:
        args = parser.parse_args(options)

    # Validate start/stop date args and convert to datetime objects.
    # Unspecified values are taken from our config.
    config = desisurvey.config.Configuration(file_name=args.config_file)
    if args.start is None:
        args.start = config.first_day()
    else:
        try:
            args.start = desisurvey.utils.get_date(args.start)
        except ValueError as e:
            raise ValueError('Invalid start: {0}'.format(e))
    if args.stop is None:
        args.stop = config.last_day()
    else:
        try:
            args.stop = desisurvey.utils.get_date(args.stop)
        except ValueError as e:
            raise ValueError('Invalid stop: {0}'.format(e))
    if args.start >= args.stop:
        raise ValueError('Expected start < stop.')

    return args
Exemplo n.º 12
0
def parse(options=None):
    """Parse command-line options for running survey planning.
    """
    parser = argparse.ArgumentParser(
        formatter_class=argparse.ArgumentDefaultsHelpFormatter)
    parser.add_argument('--verbose', action='store_true',
        help='display log messages with severity >= info')
    parser.add_argument('--debug', action='store_true',
        help='display log messages with severity >= debug (implies verbose)')
    parser.add_argument('--log-interval', type=int, default=100, metavar='N',
        help='interval for logging periodic info messages')
    parser.add_argument(
        '--exposures', default='exposures_surveysim.fits', metavar='FITS',
        help='name of FITS file with list of exposures taken')
    parser.add_argument(
        '--start', type=str, default=None, metavar='DATE',
        help='movie starts on the evening of this day, formatted as YYYY-MM-DD')
    parser.add_argument(
        '--stop', type=str, default=None, metavar='DATE',
        help='movie stops on the morning of this day, formatted as YYYY-MM-DD')
    parser.add_argument(
        '--expid', type=int, default=None, metavar='ID',
        help='index of single exposure to display')
    parser.add_argument(
        '--nightly', action='store_true',
        help='output one summary frame per night')
    # The scores option needs to be re-implemented after the refactor.
    ##parser.add_argument(
    ##    '--scores', action='store_true', help='display scheduler scores')
    parser.add_argument(
        '--save', type=str, default='surveymovie', metavar='NAME',
        help='base name (without extension) of output file to write')
    parser.add_argument(
        '--fps', type=float, default=10., metavar='FPS',
        help='frames per second to render')
    parser.add_argument(
        '--label', type=str, default='DESI', metavar='TEXT',
        help='label to display on each frame')
    parser.add_argument(
        '--output-path', default=None, metavar='PATH',
        help='path that desisurvey files are read from')
    parser.add_argument(
        '--tiles-file', default=None, metavar='TILES',
        help='name of tiles file to use instead of config.tiles_file')
    parser.add_argument(
        '--config-file', default='config.yaml', metavar='CONFIG',
        help='input configuration file')

    if options is None:
        args = parser.parse_args()
    else:
        args = parser.parse_args(options)

    # The scores option needs to be re-implemented after the refactor.
    args.scores = False

    if args.nightly and args.scores:
        log.warn('Cannot display scores in nightly summary.')
        args.scores = False

    # Validate start/stop date args and covert to datetime objects.
    # Unspecified values are taken from our config.
    config = desisurvey.config.Configuration(args.config_file)
    if args.start is None:
        args.start = config.first_day()
    else:
        try:
            args.start = desisurvey.utils.get_date(args.start)
        except ValueError as e:
            raise ValueError('Invalid start: {0}'.format(e))
    if args.stop is None:
        args.stop = config.last_day()
    else:
        try:
            args.stop = desisurvey.utils.get_date(args.stop)
        except ValueError as e:
            raise ValueError('Invalid stop: {0}'.format(e))
    if args.start >= args.stop:
        raise ValueError('Expected start < stop.')

    return args
Exemplo n.º 13
0
    def get_available_lst(self, start_date=None, stop_date=None, nbins=192, origin=-60,
                          weather=None, include_monsoon=False, include_full_moon=False,
                          include_twilight=False):
        """Calculate histograms of available LST for each program.

        Parameters
        ----------
        start_date : date or None
            First night to include or use the first date of the survey. Must
            be convertible to a date using :func:`desisurvey.utils.get_date`.
        stop_date : date or None
            First night to include or use the last date of the survey. Must
            be convertible to a date using :func:`desisurvey.utils.get_date`.
        nbins : int
            Number of LST bins to use.
        origin : float
            Rotate DEC values in plots so that the left edge is at this value
            in degrees.
        weather : array or None
            1D array of nightly weather factors (0-1) to use, or None to calculate
            available LST assuming perfect weather.  Length must equal the number
            of nights between start and stop. Values are fraction of the night
            with the dome open (0=never, 1=always). Use
            1 - :func:`desimodel.weather.dome_closed_fractions` to lookup
            suitable corrections based on historical weather data.
        include_monsoon : bool
            Include nights during the annual monsoon shutdowns.
        include_fullmoon : bool
            Include nights during the monthly full-moon breaks.
        include_twilight : bool
            Include twilight in the BRIGHT program when True.

        Returns
        -------
        tuple
            Tuple (lst_hist, lst_bins) with lst_hist having shape (3,nbins) and
            lst_bins having shape (nbins+1,).
        """
        config = desisurvey.config.Configuration()
        if start_date is None:
            start_date = config.first_day()
        else:
            start_date = desisurvey.utils.get_date(start_date)
        if stop_date is None:
            stop_date = config.last_day()
        else:
            stop_date = desisurvey.utils.get_date(stop_date)
        num_nights = (stop_date - start_date).days
        if num_nights <= 0:
            raise ValueError('Expected start_date < stop_date.')
        if weather is not None:
            weather = np.asarray(weather)
            if len(weather) != num_nights:
                raise ValueError('Expected weather array of length {}.'.format(num_nights))
        # Initialize LST histograms for each program.
        lst_bins = np.linspace(origin, origin + 360, nbins + 1)
        lst_hist = np.zeros((len(desisurvey.tiles.Tiles.PROGRAMS), nbins))
        dlst = 360. / nbins
        # Loop over nights.
        for n in range(num_nights):
            night = start_date + datetime.timedelta(n)
            if not include_monsoon and desisurvey.utils.is_monsoon(night):
                continue
            if not include_full_moon and self.is_full_moon(night):
                continue
            # Look up the program changes during this night.
            programs, changes = self.get_night_program(
                night, include_twilight, program_as_int=True)
            # Convert each change MJD to a corresponding LST in degrees.
            night_ephem = self.get_night(night)
            MJD0, MJD1 = night_ephem['brightdusk'], night_ephem['brightdawn']
            LST0, LST1 = [night_ephem['brightdusk_LST'], night_ephem['brightdawn_LST']]
            lst_changes = LST0 + (changes - MJD0) * (LST1 - LST0) / (MJD1 - MJD0)
            assert np.all(np.diff(lst_changes) > 0)
            lst_bin = (lst_changes - origin) / 360 * nbins
            # Loop over programs during the night.
            for i, prog_index in enumerate(programs):
                phist = lst_hist[prog_index]
                lo, hi = lst_bin[i:i + 2]
                # Ensure that 0 <= lo < nbins
                left_edge = np.floor(lo / nbins) * nbins
                lo -= left_edge
                hi -= left_edge
                assert 0 <= lo and lo < nbins
                ilo = int(np.ceil(lo))
                assert ilo > 0
                # Calculate the weight of this night in sidereal hours.
                wgt = 24 / nbins
                if weather is not None:
                    wgt *= weather[n]
                # Divide this program's LST window among the LST bins.
                if hi < nbins:
                    # [lo,hi) falls completely within [0,nbins)
                    ihi = int(np.floor(hi))
                    if ilo == ihi + 1:
                        # LST window is contained within a single LST bin.
                        phist[ihi] += (hi - lo) * wgt
                    else:
                        # Accumulate to bins that fall completely within the window.
                        phist[ilo:ihi] += wgt
                        # Accumulate to partial bins at each end of the program window.
                        phist[ilo - 1] += (ilo - lo) * wgt
                        phist[ihi] += (hi - ihi) * wgt
                else:
                    # [lo,hi) wraps around on the right edge.
                    hi -= nbins
                    assert hi >= 0 and hi < nbins
                    ihi = int(np.floor(hi))
                    # Accumulate to bins that fall completely within the window.
                    phist[ilo:nbins] += wgt
                    phist[0:ihi] += wgt
                    # Accumulate partial bins at each end of the program window.
                    phist[ilo - 1] += (ilo - lo) * wgt
                    phist[ihi] += (hi - ihi) * wgt
        return lst_hist, lst_bins
Exemplo n.º 14
0
    def get_program_hours(self, start_date=None, stop_date=None,
                        include_monsoon=False, include_full_moon=False,
                        include_twilight=True):
        """Tabulate hours in each program during each night of the survey.

        Use :func:`desisurvey.plots.plot_program` to visualize program hours.

        This method calculates scheduled hours with no correction for weather.
        Use 1 - :func:`desimodel.weather.dome_closed_fractions` to lookup
        nightly corrections based on historical weather data.

        Parameters
        ----------
        ephem : :class:`desisurvey.ephem.Ephemerides`
            Tabulated ephemerides data to use for determining the program.
        start_date : date or None
            First night to include or use the first date of the survey. Must
            be convertible to a date using :func:`desisurvey.utils.get_date`.
        stop_date : date or None
            First night to include or use the last date of the survey. Must
            be convertible to a date using :func:`desisurvey.utils.get_date`.
        include_monsoon : bool
            Include nights during the annual monsoon shutdowns.
        include_fullmoon : bool
            Include nights during the monthly full-moon breaks.
        include_twilight : bool
            Include twilight time at the start and end of each night in
            the BRIGHT program.

        Returns
        -------
        array
            Numpy array of shape (3, num_nights) containing the number of
            hours in each program (0=DARK, 1=GRAY, 2=BRIGHT) during each
            night.
        """
        # Determine date range to use.
        config = desisurvey.config.Configuration()
        if start_date is None:
            start_date = config.first_day()
        else:
            start_date = desisurvey.utils.get_date(start_date)
        if stop_date is None:
            stop_date = config.last_day()
        else:
            stop_date = desisurvey.utils.get_date(stop_date)
        if start_date >= stop_date:
            raise ValueError('Expected start_date < stop_date.')

        num_nights = (stop_date - start_date).days
        hours = np.zeros((3, num_nights))
        for i in range(num_nights):
            tonight = start_date + datetime.timedelta(days=i)
            if not include_monsoon and desisurvey.utils.is_monsoon(tonight):
                continue
            if not include_full_moon and self.is_full_moon(tonight):
                continue
            programs, changes = self.get_night_program(
                tonight, include_twilight=include_twilight, program_as_int=True)
            for p, dt in zip(programs, np.diff(changes)):
                hours[p, i] += dt
        hours *= 24

        return hours
Exemplo n.º 15
0
    def get_available_lst(self, start_date=None, stop_date=None, nbins=192, origin=-60,
                          weather=None, include_monsoon=False, include_full_moon=False,
                          include_twilight=False):
        """Calculate histograms of available LST for each program.

        Parameters
        ----------
        start_date : date or None
            First night to include or use the first date of the survey. Must
            be convertible to a date using :func:`desisurvey.utils.get_date`.
        stop_date : date or None
            First night to include or use the last date of the survey. Must
            be convertible to a date using :func:`desisurvey.utils.get_date`.
        nbins : int
            Number of LST bins to use.
        origin : float
            Rotate DEC values in plots so that the left edge is at this value
            in degrees.
        weather : array or None
            1D array of nightly weather factors (0-1) to use, or None to calculate
            available LST assuming perfect weather.  Length must equal the number
            of nights between start and stop. Values are fraction of the night
            with the dome open (0=never, 1=always). Use
            1 - :func:`desimodel.weather.dome_closed_fractions` to lookup
            suitable corrections based on historical weather data.
        include_monsoon : bool
            Include nights during the annual monsoon shutdowns.
        include_fullmoon : bool
            Include nights during the monthly full-moon breaks.
        include_twilight : bool
            Include twilight in the BRIGHT program when True.

        Returns
        -------
        tuple
            Tuple (lst_hist, lst_bins) with lst_hist having shape (3,nbins) and
            lst_bins having shape (nbins+1,).
        """
        config = desisurvey.config.Configuration()
        if start_date is None:
            start_date = config.first_day()
        else:
            start_date = desisurvey.utils.get_date(start_date)
        if stop_date is None:
            stop_date = config.last_day()
        else:
            stop_date = desisurvey.utils.get_date(stop_date)
        num_nights = (stop_date - start_date).days
        if num_nights <= 0:
            raise ValueError('Expected start_date < stop_date.')
        if weather is not None:
            weather = np.asarray(weather)
            if len(weather) != num_nights:
                raise ValueError('Expected weather array of length {}.'.format(num_nights))
        # Initialize LST histograms for each program.
        lst_bins = np.linspace(origin, origin + 360, nbins + 1)
        lst_hist = np.zeros((len(desisurvey.tiles.Tiles.PROGRAMS), nbins))
        dlst = 360. / nbins
        # Loop over nights.
        for n in range(num_nights):
            night = start_date + datetime.timedelta(n)
            if not include_monsoon and desisurvey.utils.is_monsoon(night):
                continue
            if not include_full_moon and self.is_full_moon(night):
                continue
            # Look up the program changes during this night.
            programs, changes = self.get_night_program(
                night, include_twilight, program_as_int=True)
            # Convert each change MJD to a corresponding LST in degrees.
            night_ephem = self.get_night(night)
            MJD0, MJD1 = night_ephem['brightdusk'], night_ephem['brightdawn']
            LST0, LST1 = [night_ephem['brightdusk_LST'], night_ephem['brightdawn_LST']]
            lst_changes = LST0 + (changes - MJD0) * (LST1 - LST0) / (MJD1 - MJD0)
            assert np.all(np.diff(lst_changes) > 0)
            lst_bin = (lst_changes - origin) / 360 * nbins
            # Loop over programs during the night.
            for i, prog_index in enumerate(programs):
                phist = lst_hist[prog_index]
                lo, hi = lst_bin[i:i + 2]
                # Ensure that 0 <= lo < nbins
                left_edge = np.floor(lo / nbins) * nbins
                lo -= left_edge
                hi -= left_edge
                assert 0 <= lo and lo < nbins
                ilo = int(np.ceil(lo))
                assert ilo > 0
                # Calculate the weight of this night in sidereal hours.
                wgt = 24 / nbins
                if weather is not None:
                    wgt *= weather[n]
                # Divide this program's LST window among the LST bins.
                if hi < nbins:
                    # [lo,hi) falls completely within [0,nbins)
                    ihi = int(np.floor(hi))
                    if ilo == ihi + 1:
                        # LST window is contained within a single LST bin.
                        phist[ihi] += (hi - lo) * wgt
                    else:
                        # Accumulate to bins that fall completely within the window.
                        phist[ilo:ihi] += wgt
                        # Accumulate to partial bins at each end of the program window.
                        phist[ilo - 1] += (ilo - lo) * wgt
                        phist[ihi] += (hi - ihi) * wgt
                else:
                    # [lo,hi) wraps around on the right edge.
                    hi -= nbins
                    assert hi >= 0 and hi < nbins
                    ihi = int(np.floor(hi))
                    # Accumulate to bins that fall completely within the window.
                    phist[ilo:nbins] += wgt
                    phist[0:ihi] += wgt
                    # Accumulate partial bins at each end of the program window.
                    phist[ilo - 1] += (ilo - lo) * wgt
                    phist[ihi] += (hi - ihi) * wgt
        return lst_hist, lst_bins
Exemplo n.º 16
0
    def get_program_hours(self, start_date=None, stop_date=None,
                        include_monsoon=False, include_full_moon=False,
                        include_twilight=True):
        """Tabulate hours in each program during each night of the survey.

        Use :func:`desisurvey.plots.plot_program` to visualize program hours.

        This method calculates scheduled hours with no correction for weather.
        Use 1 - :func:`desimodel.weather.dome_closed_fractions` to lookup
        nightly corrections based on historical weather data.

        Parameters
        ----------
        ephem : :class:`desisurvey.ephem.Ephemerides`
            Tabulated ephemerides data to use for determining the program.
        start_date : date or None
            First night to include or use the first date of the survey. Must
            be convertible to a date using :func:`desisurvey.utils.get_date`.
        stop_date : date or None
            First night to include or use the last date of the survey. Must
            be convertible to a date using :func:`desisurvey.utils.get_date`.
        include_monsoon : bool
            Include nights during the annual monsoon shutdowns.
        include_fullmoon : bool
            Include nights during the monthly full-moon breaks.
        include_twilight : bool
            Include twilight time at the start and end of each night in
            the BRIGHT program.

        Returns
        -------
        array
            Numpy array of shape (3, num_nights) containing the number of
            hours in each program (0=DARK, 1=GRAY, 2=BRIGHT) during each
            night.
        """
        # Determine date range to use.
        config = desisurvey.config.Configuration()
        if start_date is None:
            start_date = config.first_day()
        else:
            start_date = desisurvey.utils.get_date(start_date)
        if stop_date is None:
            stop_date = config.last_day()
        else:
            stop_date = desisurvey.utils.get_date(stop_date)
        if start_date >= stop_date:
            raise ValueError('Expected start_date < stop_date.')

        num_nights = (stop_date - start_date).days
        hours = np.zeros((3, num_nights))
        for i in range(num_nights):
            tonight = start_date + datetime.timedelta(days=i)
            if not include_monsoon and desisurvey.utils.is_monsoon(tonight):
                continue
            if not include_full_moon and self.is_full_moon(tonight):
                continue
            programs, changes = self.get_night_program(
                tonight, include_twilight=include_twilight, program_as_int=True)
            for p, dt in zip(programs, np.diff(changes)):
                hours[p, i] += dt
        hours *= 24

        return hours
Exemplo n.º 17
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))
Exemplo n.º 18
0
def main(args):
    """Command-line driver for running survey simulations.
    """
    # Set up the logger
    if args.debug:
        os.environ['DESI_LOGLEVEL'] = 'DEBUG'
        args.verbose = True
    elif args.verbose:
        os.environ['DESI_LOGLEVEL'] = 'INFO'
    else:
        os.environ['DESI_LOGLEVEL'] = 'WARNING'
    log = desiutil.log.get_logger()

    # 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)

    if args.existing_exposures is not None:
        exps = Table.read(args.existing_exposures)
        tiles = desisurvey.tiles.get_tiles()
        idx, mask = tiles.index(exps['TILEID'], return_mask=True)
        firstnight = max(exps['NIGHT'][mask])
        if args.start != config.first_day():
            raise ValueError('Cannot set both start and existing-exposures!')
        args.start = '-'.join(
            [firstnight[:4], firstnight[4:6], firstnight[6:]])
        args.start = desisurvey.utils.get_date(args.start)
        exps = exps[mask]
    else:
        exps = None

    # Initialize simulation progress tracking.
    stats = surveysim.stats.SurveyStatistics(args.start, args.stop)
    explist = surveysim.exposures.ExposureList(existing_exposures=exps)

    # Initialize the survey strategy rules.
    if args.rules is None:
        rulesfile = config.rules_file()
    else:
        rulesfile = args.rules
    rules = desisurvey.rules.Rules(rulesfile)
    log.info('Rules loaded from {}.'.format(rulesfile))

    # Initialize afternoon planning.
    planner = desisurvey.plan.Planner(rules, simulate=True)

    # Initialize next tile selection.
    scheduler = desisurvey.scheduler.Scheduler(planner)

    # Generate random weather conditions.
    weather = surveysim.weather.Weather(
        seed=args.seed, replay=args.replay,
        extra_downtime=args.extra_downtime)

    # Loop over nights.
    num_simulated = 0
    num_nights = (args.stop - args.start).days
    for num_simulated in range(num_nights):
        night = args.start + datetime.timedelta(num_simulated)

        if args.save_restore and num_simulated > 0:
            # Restore the planner and scheduler saved after the previous night.
            planner = desisurvey.plan.Planner(rules, restore='desi-status-{}.ecsv'.format(last_night),
                                              simulate=True)
            scheduler = desisurvey.scheduler.Scheduler(planner)

        # Perform afternoon planning.
        explist.update_tiles(night, *planner.afternoon_plan(night))

        if not desisurvey.utils.is_monsoon(night) and not scheduler.ephem.is_full_moon(night):
            # Simulate one night of observing.
            surveysim.nightops.simulate_night(
                night, scheduler, stats, explist, weather=weather,
                use_twilight=args.twilight, use_simplesky=args.simplesky)
            if scheduler.plan.survey_completed():
                log.info('Survey complete on {}.'.format(night))
                break

        if args.save_restore:
            last_night = desisurvey.utils.night_to_str(night)
            planner.save('desi-status-{}.ecsv'.format(last_night))

        if num_simulated % args.log_interval == args.log_interval - 1:
            log.info('Completed {} / {} tiles after {} / {} nights.'.format(
                scheduler.plan.obsend().sum(),
                scheduler.tiles.ntiles,
                num_simulated + 1, num_nights))

    explist.save('exposures_{}.fits'.format(args.name), comment=args.comment)
    stats.save('stats_{}.fits'.format(args.name), comment=args.comment)
    planner.save('desi-status-end-{}.ecsv'.format(args.name))
    if args.verbose:
        stats.summarize()
Exemplo n.º 19
0
def plot_program(ephem, start_date=None, stop_date=None, style='localtime',
                 include_monsoon=False, include_full_moon=False,
                 include_twilight=True, night_start=-6.5, night_stop=7.5,
                 num_points=500, bg_color='lightblue', save=None):
    """Plot an overview of the DARK/GRAY/BRIGHT program.

    Uses :func:`desisurvey.ephem.get_program_hours` to calculate the
    hours available for each program during each night.

    The matplotlib and basemap packages must be installed to use this function.

    Parameters
    ----------
    ephem : :class:`desisurvey.ephem.Ephemerides`
        Tabulated ephemerides data to use for determining the program.
    start_date : date or None
        First night to include in the plot or use the first date of the
        survey.  Must be convertible to a
        date using :func:`desisurvey.utils.get_date`.
    stop_date : date or None
        First night to include in the plot or use the last date of the
        survey.  Must be convertible to a
        date using :func:`desisurvey.utils.get_date`.
    style : string
        Plot style to use for the vertical axis: "localtime" shows time
        relative to local midnight, "histogram" shows elapsed time for
        each program during each night, and "cumulative" shows the
        cummulative time for each program since ``start_date``.
    include_monsoon : bool
        Include nights during the annual monsoon shutdowns.
    include_fullmoon : bool
        Include nights during the monthly full-moon breaks.
    include_twilight : bool
        Include twilight time at the start and end of each night in
        the BRIGHT program.
    night_start : float
        Start of night in hours relative to local midnight used to set
        y-axis minimum for 'localtime' style and tabulate nightly program.
    night_stop : float
        End of night in hours relative to local midnight used to set
        y-axis maximum for 'localtime' style and tabulate nightly program.
    num_points : int
        Number of subdivisions of the vertical axis to use for tabulating
        the program during each night. The resulting resolution will be
        ``(night_stop - night_start) / num_points`` hours.
    bg_color : matplotlib color
        Axis background color to use.  Must be a valid matplotlib color.
    save : string or None
        Name of file where plot should be saved.  Format is inferred from
        the extension.

    Returns
    -------
    tuple
        Tuple (figure, axes) returned by ``plt.subplots()``.
    """
    import matplotlib.pyplot as plt
    import matplotlib.colors
    import matplotlib.dates
    import matplotlib.ticker
    import pytz

    styles = ('localtime', 'histogram', 'cumulative')
    if style not in styles:
        raise ValueError('Valid styles are {0}.'.format(', '.join(styles)))

    hours = ephem.get_program_hours(
        start_date, stop_date, include_monsoon,
        include_full_moon, include_twilight)
    observing_night = hours.sum(axis=0) > 0

    # Determine plot date range.
    config = desisurvey.config.Configuration()
    if start_date is None:
        start_date = config.first_day()
    else:
        start_date = desisurvey.utils.get_date(start_date)
    if stop_date is None:
        stop_date = config.last_day()
    else:
        stop_date = desisurvey.utils.get_date(stop_date)
    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)

    # Matplotlib date axes uses local time and puts ticks between days
    # at local midnight. We explicitly specify UTC for x-axis labels so
    # that the plot does not depend on the caller's local timezone.
    tz = pytz.utc
    midnight = datetime.time(hour=0)
    xaxis_start = tz.localize(datetime.datetime.combine(start_date, midnight))
    xaxis_stop = tz.localize(datetime.datetime.combine(stop_date, midnight))
    xaxis_lo = matplotlib.dates.date2num(xaxis_start)
    xaxis_hi = matplotlib.dates.date2num(xaxis_stop)

    # Build a grid of elapsed time relative to local midnight during each night.
    midnight = t['noon'] + 0.5
    t_edges = np.linspace(night_start, night_stop, num_points + 1) / 24.
    t_centers = 0.5 * (t_edges[1:] + t_edges[:-1])

    # Initialize the plot.
    fig, ax = plt.subplots(1, 1, figsize=(11, 8.5), squeeze=True)

    if style == 'localtime':

        # Loop over nights to build image data to plot.
        program = np.zeros((num_nights, len(t_centers)))
        for i in np.arange(num_nights):
            if not observing_night[i]:
                continue
            mjd_grid = midnight[i] + t_centers
            dark, gray, bright = ephem.tabulate_program(mjd_grid)
            program[i][dark] = 1.
            program[i][gray] = 2.
            program[i][bright] = 3.

        # Prepare a custom colormap.
        colors = [bg_color, program_color['DARK'],
                  program_color['GRAY'], program_color['BRIGHT']]
        cmap = matplotlib.colors.ListedColormap(colors, 'programs')

        ax.imshow(program.T, origin='lower', interpolation='none',
                  aspect='auto', cmap=cmap, vmin=-0.5, vmax=+3.5,
                  extent=[xaxis_lo, xaxis_hi, night_start, night_stop])

        # Display 24-hour local time on y axis.
        y_lo = int(np.ceil(night_start))
        y_hi = int(np.floor(night_stop))
        y_ticks = np.arange(y_lo, y_hi + 1, dtype=int)
        y_labels = ['{:02d}h'.format(hr) for hr in (24 + y_ticks) % 24]
        config = desisurvey.config.Configuration()
        ax.set_ylabel('Local Time [{0}]'
                      .format(config.location.timezone()), fontsize='x-large')
        ax.set_yticks(y_ticks)
        ax.set_yticklabels(y_labels)

    else:

        x = xaxis_lo + np.arange(num_nights) + 0.5
        y = hours if style == 'histogram' else np.cumsum(hours, axis=1)
        size = min(15., (300./num_nights) ** 2)
        opts = dict(linestyle='-', marker='.' if size > 1 else None,
                    markersize=size)
        ax.plot(x, y[0], color=program_color['DARK'], **opts)
        ax.plot(x, y[1], color=program_color['GRAY'], **opts)
        ax.plot(x, y[2], color=program_color['BRIGHT'], **opts)

        ax.set_facecolor(bg_color)
        ax.set_ylim(0, 1.07 * y.max())
        if style == 'histogram':
            ax.set_ylabel('Hours / Night')
        else:
            ax.set_ylabel('Cumulative Hours')

    # Display dates on the x axis.
    ax.set_xlabel('Survey Date (observing {0} / {1} nights)'
                  .format(np.count_nonzero(observing_night), num_nights),
                  fontsize='x-large')
    ax.set_xlim(xaxis_start, xaxis_stop)
    if num_nights < 50:
        # Major ticks at month boundaries.
        ax.xaxis.set_major_locator(matplotlib.dates.MonthLocator(tz=tz))
        ax.xaxis.set_major_formatter(
            matplotlib.dates.DateFormatter('%m/%y', tz=tz))
        # Minor ticks at day boundaries.
        ax.xaxis.set_minor_locator(matplotlib.dates.DayLocator(tz=tz))
    elif num_nights <= 650:
        # Major ticks at month boundaries with no labels.
        ax.xaxis.set_major_locator(matplotlib.dates.MonthLocator(tz=tz))
        ax.xaxis.set_major_formatter(matplotlib.ticker.NullFormatter())
        # Minor ticks at month midpoints with centered labels.
        ax.xaxis.set_minor_locator(
            matplotlib.dates.MonthLocator(bymonthday=15, tz=tz))
        ax.xaxis.set_minor_formatter(
            matplotlib.dates.DateFormatter('%m/%y', tz=tz))
        for tick in ax.xaxis.get_minor_ticks():
            tick.tick1line.set_markersize(0)
            tick.tick2line.set_markersize(0)
            tick.label1.set_horizontalalignment('center')
    else:
        # Major ticks at year boundaries.
        ax.xaxis.set_major_locator(matplotlib.dates.YearLocator(tz=tz))
        ax.xaxis.set_major_formatter(
            matplotlib.dates.DateFormatter('%Y', tz=tz))

    ax.grid(b=True, which='major', color='w', linestyle=':', lw=1)

    # Draw program labels.
    y = 0.975
    opts = dict(fontsize='xx-large', fontweight='bold', xy=(0, 0),
                horizontalalignment='center', verticalalignment='top',
                xycoords='axes fraction', textcoords='axes fraction')
    ax.annotate('DARK {0:.1f}h'.format(hours[0].sum()), xytext=(0.2, y),
                color=program_color['DARK'], **opts)
    ax.annotate('GRAY {0:.1f}h'.format(hours[1].sum()), xytext=(0.5, y),
                color=program_color['GRAY'], **opts)
    ax.annotate(
        'BRIGHT {0:.1f}h'.format(hours[2].sum()), xytext=(0.8, y),
        color=program_color['BRIGHT'], **opts)

    plt.tight_layout()
    if save:
        plt.savefig(save)

    return fig, ax
Exemplo n.º 20
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
Exemplo n.º 21
0
def parse(options=None):
    """Parse command-line options for running survey simulations.
    """
    parser = argparse.ArgumentParser(
        formatter_class=argparse.ArgumentDefaultsHelpFormatter)
    parser.add_argument('--verbose',
                        action='store_true',
                        help='display log messages with severity >= info')
    parser.add_argument(
        '--debug',
        action='store_true',
        help='display log messages with severity >= debug (implies verbose)')
    parser.add_argument(
        '--log-interval',
        type=int,
        default=100,
        metavar='N',
        help='nightly interval for logging periodic info messages')
    parser.add_argument(
        '--start',
        type=str,
        default=None,
        metavar='DATE',
        help='survey starts on the evening of this day, formatted as YYYY-MM-DD'
    )
    parser.add_argument(
        '--stop',
        type=str,
        default=None,
        metavar='DATE',
        help='survey stops on the morning of this day, formatted as YYYY-MM-DD'
    )
    parser.add_argument(
        '--name',
        type=str,
        default='surveysim',
        metavar='NAME',
        help='name to use for saving simulated stats and exposures')
    parser.add_argument(
        '--comment',
        type=str,
        default='',
        metavar='COMMENT',
        help='comment to save with simulated stats and exposures')
    parser.add_argument(
        '--rules',
        type=str,
        default='rules.yaml',
        metavar='YAML',
        help='name of YAML file with survey strategy rules to use')
    parser.add_argument('--twilight',
                        action='store_true',
                        help='include twilight in the scheduled time')
    parser.add_argument(
        '--save-restore',
        action='store_true',
        help='save/restore the planner and scheduler state after each night')
    parser.add_argument(
        '--seed',
        type=int,
        default=1,
        metavar='N',
        help='random number seed for generating random observing conditions')
    parser.add_argument(
        '--replay',
        type=str,
        default='random',
        metavar='REPLAY',
        help='Replay specific weather years, e.g., "Y2015,Y2011" or "random"')
    parser.add_argument(
        '--output-path',
        default=None,
        metavar='PATH',
        help='output path to use instead of config.output_path')
    parser.add_argument(
        '--tiles-file',
        default=None,
        metavar='TILES',
        help='name of tiles file to use instead of config.tiles_file')
    parser.add_argument('--config-file',
                        default='config.yaml',
                        metavar='CONFIG',
                        help='input configuration file')

    if options is None:
        args = parser.parse_args()
    else:
        args = parser.parse_args(options)

    # Validate start/stop date args and convert to datetime objects.
    # Unspecified values are taken from our config.
    config = desisurvey.config.Configuration(file_name=args.config_file)
    if args.start is None:
        args.start = config.first_day()
    else:
        try:
            args.start = desisurvey.utils.get_date(args.start)
        except ValueError as e:
            raise ValueError('Invalid start: {0}'.format(e))
    if args.stop is None:
        args.stop = config.last_day()
    else:
        try:
            args.stop = desisurvey.utils.get_date(args.stop)
        except ValueError as e:
            raise ValueError('Invalid stop: {0}'.format(e))
    if args.start >= args.stop:
        raise ValueError('Expected start < stop.')

    return args
Exemplo n.º 22
0
def initialize(ephem, start_date=None, stop_date=None, step_size=5.0,
               healpix_nside=16, output_name='scheduler.fits'):
    """Calculate exposure-time factors over a grid of times and pointings.

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

    Requires that healpy is installed.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    # Finalize the output file.
    try:
        hdus.writeto(output_name, overwrite=True)
    except TypeError:
        # astropy < 1.3 uses the now deprecated clobber.
        hdus.writeto(output_name, clobber=True)
    log.info('Plan initialization saved to {0}'.format(output_name))
Exemplo n.º 23
0
def parse(options=None):
    """Parse command-line options for running survey planning.
    """
    parser = argparse.ArgumentParser(
        formatter_class=argparse.ArgumentDefaultsHelpFormatter)
    parser.add_argument('--verbose',
                        action='store_true',
                        help='display log messages with severity >= info')
    parser.add_argument(
        '--debug',
        action='store_true',
        help='display log messages with severity >= debug (implies verbose)')
    parser.add_argument('--log-interval',
                        type=int,
                        default=100,
                        metavar='N',
                        help='interval for logging periodic info messages')
    parser.add_argument('--exposures',
                        default='exposures_surveysim.fits',
                        metavar='FITS',
                        help='name of FITS file with list of exposures taken')
    parser.add_argument(
        '--start',
        type=str,
        default=None,
        metavar='DATE',
        help='movie starts on the evening of this day, formatted as YYYY-MM-DD'
    )
    parser.add_argument(
        '--stop',
        type=str,
        default=None,
        metavar='DATE',
        help='movie stops on the morning of this day, formatted as YYYY-MM-DD')
    parser.add_argument('--expid',
                        type=int,
                        default=None,
                        metavar='ID',
                        help='index of single exposure to display')
    parser.add_argument('--nightly',
                        action='store_true',
                        help='output one summary frame per night')
    # The scores option needs to be re-implemented after the refactor.
    ##parser.add_argument(
    ##    '--scores', action='store_true', help='display scheduler scores')
    parser.add_argument(
        '--save',
        type=str,
        default='surveymovie',
        metavar='NAME',
        help='base name (without extension) of output file to write')
    parser.add_argument('--fps',
                        type=float,
                        default=10.,
                        metavar='FPS',
                        help='frames per second to render')
    parser.add_argument('--label',
                        type=str,
                        default='DESI',
                        metavar='TEXT',
                        help='label to display on each frame')
    parser.add_argument('--output-path',
                        default=None,
                        metavar='PATH',
                        help='path that desisurvey files are read from')
    parser.add_argument(
        '--tiles-file',
        default=None,
        metavar='TILES',
        help='name of tiles file to use instead of config.tiles_file')
    parser.add_argument('--config-file',
                        default='config.yaml',
                        metavar='CONFIG',
                        help='input configuration file')

    if options is None:
        args = parser.parse_args()
    else:
        args = parser.parse_args(options)

    # The scores option needs to be re-implemented after the refactor.
    args.scores = False

    if args.nightly and args.scores:
        log.warn('Cannot display scores in nightly summary.')
        args.scores = False

    # Validate start/stop date args and covert to datetime objects.
    # Unspecified values are taken from our config.
    config = desisurvey.config.Configuration(args.config_file)
    if args.start is None:
        args.start = config.first_day()
    else:
        try:
            args.start = desisurvey.utils.get_date(args.start)
        except ValueError as e:
            raise ValueError('Invalid start: {0}'.format(e))
    if args.stop is None:
        args.stop = config.last_day()
    else:
        try:
            args.stop = desisurvey.utils.get_date(args.stop)
        except ValueError as e:
            raise ValueError('Invalid stop: {0}'.format(e))
    if args.start >= args.stop:
        raise ValueError('Expected start < stop.')

    return args
Exemplo n.º 24
0
    def __init__(self,
                 start_date=None,
                 stop_date=None,
                 use_twilight=False,
                 weather=None,
                 design_hourangle=None):
        config = desisurvey.config.Configuration()
        if start_date is None:
            start_date = config.first_day()
        else:
            start_date = desisurvey.utils.get_date(start_date)
        if stop_date is None:
            stop_date = config.last_day()
        else:
            stop_date = desisurvey.utils.get_date(stop_date)
        self.num_nights = (stop_date - start_date).days
        if self.num_nights <= 0:
            raise ValueError('Expected start_date < stop_date.')

        self.use_twilight = use_twilight
        # Look up the tiles to observe.
        tiles = desisurvey.tiles.get_tiles()
        self.tiles = tiles
        if design_hourangle is None:
            self.design_hourangle = np.zeros(tiles.ntiles)
        else:
            if len(design_hourangle) != tiles.ntiles:
                raise ValueError('Array design_hourangle has wrong length.')
            self.design_hourangle = np.asarray(design_hourangle)
        # Get weather factors.
        if weather is None:
            self.weather = desisurvey.plan.load_weather(start_date, stop_date)
        else:
            self.weather = np.asarray(weather)
        if self.weather.shape != (self.num_nights, ):
            raise ValueError('Array weather has wrong shape.')
        # Get the design hour angles.
        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 != (tiles.ntiles, ):
            raise ValueError('Array design_hourangle has wrong shape.')
        # Compute airmass at design hour angles.
        self.airmass = tiles.airmass(self.design_hourangle)
        airmass_factor = desisurvey.etc.airmass_exposure_factor(self.airmass)
        # Load ephemerides.
        ephem = desisurvey.ephem.get_ephem()
        # Compute the expected available and scheduled hours per program.
        scheduled = ephem.get_program_hours(include_twilight=use_twilight)
        available = scheduled * self.weather
        self.cummulative_days = np.cumsum(available, axis=1) / 24.
        # Calculate program parameters.
        ntiles, tsched, openfrac, dust, airmass, nominal = [], [], [], [], [], []
        for program in tiles.PROGRAMS:
            tile_sel = tiles.program_mask[program]
            ntiles.append(np.count_nonzero(tile_sel))
            progindx = tiles.PROGRAM_INDEX[program]
            scheduled_sum = scheduled[progindx].sum()
            tsched.append(scheduled_sum)
            openfrac.append(available[progindx].sum() / scheduled_sum)
            dust.append(tiles.dust_factor[tile_sel].mean())
            airmass.append(airmass_factor[tile_sel].mean())
            nominal.append(
                getattr(config.nominal_exposure_time, program)().to(u.s).value)
        # Build a table of all forecasting parameters.
        df = collections.OrderedDict()
        self.df = df
        df['Number of tiles'] = np.array(ntiles)
        df['Scheduled time (hr)'] = np.array(tsched)
        df['Dome open fraction'] = np.array(openfrac)
        self.set_overheads()
        df['Nominal exposure (s)'] = np.array(nominal)
        df['Dust factor'] = np.array(dust)
        df['Airmass factor'] = np.array(airmass)
        self.set_factors()