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