def test_end_to_end(self): cmd = 'surveyinit --max-cycles 5 --init zero' args = desisurvey.scripts.surveyinit.parse(cmd.split()[1:]) desisurvey.scripts.surveyinit.main(args) cmd = 'surveysim --rules rules-layers.yaml --name test' args = surveysim.scripts.surveysim.parse(cmd.split()[1:]) surveysim.scripts.surveysim.main(args) config = desisurvey.config.Configuration() self.assertTrue(os.path.exists(config.get_path('stats_test.fits'))) self.assertTrue(os.path.exists(config.get_path('exposures_test.fits'))) stats = surveysim.stats.SurveyStatistics(restore='stats_test.fits') exposures = surveysim.exposures.ExposureList(restore='exposures_test.fits')
def test_end_to_end(self): cmd = 'surveyinit --max-cycles 5 --init zero' args = desisurvey.scripts.surveyinit.parse(cmd.split()[1:]) desisurvey.scripts.surveyinit.main(args) cmd = 'surveysim --rules rules-layers.yaml --name test' args = surveysim.scripts.surveysim.parse(cmd.split()[1:]) surveysim.scripts.surveysim.main(args) config = desisurvey.config.Configuration() self.assertTrue(os.path.exists(config.get_path('stats_test.fits'))) self.assertTrue(os.path.exists(config.get_path('exposures_test.fits'))) stats = surveysim.stats.SurveyStatistics(restore='stats_test.fits') exposures = surveysim.exposures.ExposureList( restore='exposures_test.fits')
def save(self, name): """Save a snapshot of our current state that can be restored. The only internal state required to restore a Scheduler is the array of snr2frac values per tile. The snapshot file size is about 130Kb. Parameters ---------- name : str Name of FITS file where the snapshot will be saved. The file will be saved under our configuration's output path unless name is already an absolute path. Pass the same name to the constructor's ``restore`` argument to restore this snapshot. """ config = desisurvey.config.Configuration() fullname = config.get_path(name) hdr = astropy.io.fits.Header() # Record the last night this scheduler was initialized for. hdr['NIGHT'] = self.night.isoformat() if self.night else '' # Record the number of completed tiles. hdr['NDONE'] = self.completed_by_pass.sum() # Save a copy of our snr2frac array. astropy.io.fits.PrimaryHDU(self.snr2frac, header=hdr).writeto(fullname, overwrite=True) self.log.debug('Saved scheduler snapshot to "{}".'.format(fullname))
def save(self, name): """Save a snapshot of our current state that can be restored. The only internal state required to restore a Scheduler is the array of snr2frac values per tile. The snapshot file size is about 130Kb. Parameters ---------- name : str Name of FITS file where the snapshot will be saved. The file will be saved under our configuration's output path unless name is already an absolute path. Pass the same name to the constructor's ``restore`` argument to restore this snapshot. """ config = desisurvey.config.Configuration() fullname = config.get_path(name) hdr = astropy.io.fits.Header() # Record the last night this scheduler was initialized for. hdr['NIGHT'] = self.night.isoformat() if self.night else '' # Record the number of completed tiles. hdr['NDONE'] = self.completed_by_pass.sum() # Save a copy of our snr2frac array. astropy.io.fits.PrimaryHDU(self.snr2frac, header=hdr).writeto(fullname+'.tmp', overwrite=True) os.rename(fullname+'.tmp', fullname) self.log.debug('Saved scheduler snapshot to "{}".'.format(fullname))
def save(self, name='stats.fits', comment='', overwrite=True): """Save a snapshot of these statistics as a binary FITS table. The saved file size is ~800 Kb. Parameters ---------- name : str File name to write. Will be located in the configuration output path unless it is an absolute path. Pass the same name to the constructor's ``restore`` argument to restore this snapshot. comment : str Comment to include in the saved header, for documentation purposes. overwrite : bool Silently overwrite any existing file when True. """ hdus = astropy.io.fits.HDUList() header = astropy.io.fits.Header() header['TILES'] = self.tiles.tiles_file header['START'] = self.start_date.isoformat() header['STOP'] = self.stop_date.isoformat() header['COMMENT'] = comment header['EXTNAME'] = 'STATS' hdus.append(astropy.io.fits.PrimaryHDU()) hdus.append(astropy.io.fits.BinTableHDU(self._data, header=header, name='STATS')) config = desisurvey.config.Configuration() fullname = config.get_path(name) hdus.writeto(fullname, overwrite=overwrite) log = desiutil.log.get_logger() log.info('Saved stats to {}'.format(fullname)) if comment: log.info('Saved with comment "{}".'.format(header['COMMENT']))
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 get_ephem(use_cache=True, write_cache=True): """Return tabulated ephemerides for (START_DATE,STOP_DATE). The pyephem module must be installed to calculate ephemerides, but is not necessary when a FITS file of precalcuated data is available. Parameters ---------- use_cache : bool Use cached ephemerides from memory or disk if possible when True. Otherwise, always calculate from scratch. write_cache : bool When True, write a generated table so it is available for future invocations. Writing only takes place when a cached object is not available or ``use_cache`` is False. Returns ------- Ephemerides Object with tabulated ephemerides for (START_DATE,STOP_DATE). """ global _ephem # Freeze IERS table for consistent results. desisurvey.utils.freeze_iers() # Use standardized string representation of dates. start_iso = START_DATE.isoformat() stop_iso = STOP_DATE.isoformat() range_iso = '({},{})'.format(start_iso, stop_iso) log = desiutil.log.get_logger() # First check for a cached object in memory. if use_cache and _ephem is not None: if _ephem.start_date != START_DATE or _ephem.stop_date != STOP_DATE: raise RuntimeError('START_DATE, STOP_DATE have changed.') log.debug('Returning cached ephemerides for {}.'.format(range_iso)) return _ephem # Next check for a FITS file on disk. config = desisurvey.config.Configuration() filename = config.get_path('ephem_{}_{}.fits'.format(start_iso, stop_iso)) if use_cache and os.path.exists(filename): # Save restored object in memory. _ephem = Ephemerides(START_DATE, STOP_DATE, restore=filename) log.info('Restored ephemerides for {} from {}.' .format(range_iso, filename)) return _ephem # Finally, create new ephemerides and save in the memory cache. log.info('Building ephemerides for {}...'.format(range_iso)) _ephem = Ephemerides(START_DATE, STOP_DATE) if write_cache: # Save the tabulated ephemerides to disk. _ephem._table.write(filename, overwrite=True) log.info('Saved ephemerides for {} to {}'.format(range_iso, filename)) return _ephem
def get_ephem(use_cache=True, write_cache=True): """Return tabulated ephemerides for (START_DATE,STOP_DATE). The pyephem module must be installed to calculate ephemerides, but is not necessary when a FITS file of precalcuated data is available. Parameters ---------- use_cache : bool Use cached ephemerides from memory or disk if possible when True. Otherwise, always calculate from scratch. write_cache : bool When True, write a generated table so it is available for future invocations. Writing only takes place when a cached object is not available or ``use_cache`` is False. Returns ------- Ephemerides Object with tabulated ephemerides for (START_DATE,STOP_DATE). """ global _ephem # Freeze IERS table for consistent results. desisurvey.utils.freeze_iers() # Use standardized string representation of dates. start_iso = START_DATE.isoformat() stop_iso = STOP_DATE.isoformat() range_iso = '({},{})'.format(start_iso, stop_iso) log = desiutil.log.get_logger() # First check for a cached object in memory. if use_cache and _ephem is not None: if _ephem.start_date != START_DATE or _ephem.stop_date != STOP_DATE: raise RuntimeError('START_DATE, STOP_DATE have changed.') log.debug('Returning cached ephemerides for {}.'.format(range_iso)) return _ephem # Next check for a FITS file on disk. config = desisurvey.config.Configuration() filename = config.get_path('ephem_{}_{}.fits'.format(start_iso, stop_iso)) if use_cache and os.path.exists(filename): # Save restored object in memory. _ephem = Ephemerides(START_DATE, STOP_DATE, restore=filename) log.info('Restored ephemerides for {} from {}.' .format(range_iso, filename)) return _ephem # Finally, create new ephemerides and save in the memory cache. log.info('Building ephemerides for {}...'.format(range_iso)) _ephem = Ephemerides(START_DATE, STOP_DATE) if write_cache: # Save the tabulated ephemerides to disk. _ephem._table.write(filename, overwrite=True) log.info('Saved ephemerides for {} to {}'.format(range_iso, filename)) return _ephem
def load(name, extra_nexp=0): config = desisurvey.config.Configuration() name = config.get_path(name) with astropy.io.fits.open(name) as hdus: header = hdus[0].header comment = header['COMMENT'] nexp = header['NEXP'] max_nexp = nexp + extra_nexp explist = ExposureList(max_nexp=max_nexp) explist._exposures[:nexp] = hdus['EXPOSURES'].data explist._tiledata[:] = hdus['TILEDATA'].data log = desiutil.log.get_logger() log.info('Loaded {} exposures from {}'.format(nexp, name)) if comment: log.info('Loaded with comment "{}".'.format(comment)) return explist
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 __init__(self, restore=None, max_nexp=60000, existing_exposures=None): self.tiles = desisurvey.tiles.get_tiles() self._exposures = np.empty(max_nexp, dtype=[ ('EXPID', np.int32), ('MJD', np.float64), ('EXPTIME', np.float32), ('TILEID', np.int32), ('SNR2FRAC', np.float32), ('DSNR2FRAC', np.float32), ('AIRMASS', np.float32), ('SEEING', np.float32), ('TRANSP', np.float32), ('SKY', np.float32), ]) self._tiledata = np.empty(self.tiles.ntiles, dtype=[ ('AVAIL', np.int32), ('PLANNED', np.int32), ('EXPTIME', np.float32), ('SNR2FRAC', np.float32), ('NEXP', np.int32) ]) if restore is not None: config = desisurvey.config.Configuration() fullname = config.get_path(restore) with astropy.io.fits.open(fullname, memmap=False) as hdus: header = hdus[0].header comment = header['COMMENT'] self.nexp = header['NEXP'] self._exposures[:self.nexp] = hdus['EXPOSURES'].data self._tiledata[:] = hdus['TILEDATA'].data self.initial_night = desisurvey.utils.get_date(header['INITIAL']) if header['INITIAL'] else None log = desiutil.log.get_logger() log.info('Restored stats from {}'.format(fullname)) if comment: log.info(' Comment: "{}".'.format(comment)) else: self.nexp = 0 self._tiledata['AVAIL'] = -1 self._tiledata['PLANNED'] = -1 self._tiledata['EXPTIME'] = 0. self._tiledata['SNR2FRAC'] = 0. self._tiledata['NEXP'] = 0 self.initial_night = None if existing_exposures is not None: self.add_existing_exposures(existing_exposures)
def save(self, filename, overwrite=True): """Save the generated weather to a file. The saved file can be restored using the constructor `restore` parameter. Parameters ---------- filename : str Name of the file where the weather should be saved. A relative path name refers to the :meth:`configuration output path <desisurvey.config.Configuration.get_path>`. overwrite : bool Silently overwrite any existing file when this is True. """ config = desisurvey.config.Configuration() filename = config.get_path(filename) self._table.write(filename, overwrite=overwrite) self.log.info('Saved weather to {0}.'.format(filename))
def save(self, filename, overwrite=True): """Save the generated weather to a file. The saved file can be restored using the constructor `restore` parameter. Parameters ---------- filename : str Name of the file where the weather should be saved. A relative path name refers to the :meth:`configuration output path <desisurvey.config.Configuration.get_path>`. overwrite : bool Silently overwrite any existing file when this is True. """ config = desisurvey.config.Configuration() filename = config.get_path(filename) self._table.write(filename, overwrite=overwrite) self.log.info('Saved weather to {0}.'.format(filename))
def save(self, filename, overwrite=True): """Save the current progress to a file. The saved file can be restored from disk using our constructor, although column descriptions will be lost since they are not propagated when writing a table to a FITS file. Parameters ---------- filename : str Name of the file where the progress record should be saved. A relative path name refers to the :meth:`configuration output path <desisurvey.config.Configuration.get_path>`. overwrite : bool Silently overwrite any existing file when this is True. """ config = desisurvey.config.Configuration() filename = config.get_path(filename) self._table.write(filename, overwrite=overwrite) self.log.info('Saved progress to {0}.'.format(filename))
def save(self, name='exposures.fits', comment='', overwrite=True): """Save exposures to a FITS file with two binary tables. The saved file size scales linearly with the number of exposures added so far, and is independent of the memory size of this object. Parameters ---------- name : str File name to write. Will be located in the configuration output path unless it is an absolute path. Pass the same name to the constructor's ``restore`` argument to restore this snapshot. comment : str Comment to include in the saved header, for documentation purposes. overwrite : bool Silently overwrite any existing file when True. """ hdus = astropy.io.fits.HDUList() header = astropy.io.fits.Header() header['TILES'] = self.tiles.tiles_file header['NEXP'] = self.nexp header['COMMENT'] = comment header['INITIAL'] = self.initial_night.isoformat( ) if self.initial_night else '' header['EXTNAME'] = 'META' hdus.append(astropy.io.fits.PrimaryHDU(header=header)) hdus.append( astropy.io.fits.BinTableHDU(self._exposures[:self.nexp], name='EXPOSURES')) hdus.append( astropy.io.fits.BinTableHDU(self._tiledata, name='TILEDATA')) config = desisurvey.config.Configuration() name = config.get_path(name) hdus.writeto(name, overwrite=overwrite) log = desiutil.log.get_logger() log.info('Saved {} exposures to {}'.format(self.nexp, name)) if comment: log.info('Saved with comment "{}".'.format(header['COMMENT']))
def save(self, name='stats.fits', comment='', overwrite=True): """Save a snapshot of these statistics as a binary FITS table. The saved file size is ~800 Kb. Parameters ---------- name : str File name to write. Will be located in the configuration output path unless it is an absolute path. Pass the same name to the constructor's ``restore`` argument to restore this snapshot. comment : str Comment to include in the saved header, for documentation purposes. overwrite : bool Silently overwrite any existing file when True. """ hdus = astropy.io.fits.HDUList() header = astropy.io.fits.Header() header['TILES'] = self.tiles.tiles_file header['START'] = self.start_date.isoformat() header['STOP'] = self.stop_date.isoformat() header['COMMENT'] = comment header['EXTNAME'] = 'STATS' hdus.append(astropy.io.fits.PrimaryHDU()) hdus.append( astropy.io.fits.BinTableHDU(self._data, header=header, name='STATS')) config = desisurvey.config.Configuration() fullname = config.get_path(name) hdus.writeto(fullname, overwrite=overwrite) log = desiutil.log.get_logger() log.info('Saved stats to {}'.format(fullname)) if comment: log.info('Saved with comment "{}".'.format(header['COMMENT']))
def main(args): """Command-line driver to visualize survey scheduling and progress. """ # Set up the logger if args.debug: log = desiutil.log.get_logger(desiutil.log.DEBUG) args.verbose = True elif args.verbose: log = desiutil.log.get_logger(desiutil.log.INFO) else: log = desiutil.log.get_logger(desiutil.log.WARNING) # Freeze IERS table for consistent results. desisurvey.utils.freeze_iers() # Set the output path if requested. config = desisurvey.config.Configuration() if args.output_path is not None: config.set_output_path(args.output_path) if args.tiles_file is not None: config.tiles_file.set_value(args.tiles_file) # Look for the exposures file in the output path by default. args.exposures = config.get_path(args.exposures) # Initialize. animator = Animator( args.exposures, args.start, args.stop, args.label, args.scores) log.info('Found {0} exposures from {1} to {2} ({3} nights).' .format(animator.num_exp, args.start, args.stop, animator.num_nights)) animator.init_figure(args.nightly) if args.expid is not None: expid = animator.exposures['EXPID'] assert np.all(expid == expid[0] + np.arange(len(expid))) if (args.expid < expid[0]) or (args.expid > expid[-1]): raise RuntimeError('Requested exposure ID {0} not available.' .format(args.expid)) animator.draw_exposure(args.expid - expid[0], args.nightly) save_name = args.save + '.png' plt.savefig(save_name) log.info('Saved {0}.'.format(save_name)) else: nframes = animator.num_nights if args.nightly else animator.num_exp iexp = [0] def init(): return animator.artists def update(iframe): if (iframe + 1) % args.log_interval == 0: log.info('Drawing frame {0}/{1}.' .format(iframe + 1, nframes)) if args.nightly: while not animator.draw_exposure(iexp[0], nightly=True): iexp[0] += 1 else: animator.draw_exposure(iexp=iframe, nightly=False) return animator.artists log.info('Movie will be {:.1f} mins long at {:.1f} frames/sec.' .format(nframes / (60 * args.fps), args.fps)) animation = matplotlib.animation.FuncAnimation( animator.figure, update, init_func=init, blit=True, frames=nframes) writer = matplotlib.animation.writers['ffmpeg']( bitrate=2400, fps=args.fps, metadata=dict(artist='surveymovie')) save_name = args.save + '.mp4' animation.save(save_name, writer=writer, dpi=animator.dpi) log.info('Saved {0}.'.format(save_name))
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 __init__(self, restore=None, max_exposures=32): self.log = desiutil.log.get_logger() # Lookup the completeness SNR2 threshold to use. config = desisurvey.config.Configuration() self.min_snr2 = config.min_snr2_fraction() if restore is None: # Load the list of tiles to observe. tiles = astropy.table.Table( desimodel.io.load_tiles(onlydesi=True, extra=False, tilesfile=config.tiles_file() )) num_tiles = len(tiles) # Initialize a new progress table. meta = dict(VERSION=_version) table = astropy.table.Table(meta=meta) table['tileid'] = astropy.table.Column( length=num_tiles, dtype=np.int32, description='DESI footprint tile ID') table['pass'] = astropy.table.Column( length=num_tiles, dtype=np.int32, description='Observing pass number starting at zero') table['ra'] = astropy.table.Column( length=num_tiles, description='TILE center RA in degrees', unit='deg', format='%.1f') table['dec'] = astropy.table.Column( length=num_tiles, description='TILE center DEC in degrees', unit='deg', format='%.1f') table['status'] = astropy.table.Column( length=num_tiles, dtype=np.int32, description='Observing status: 0=none, 1=partial, 2=done') table['covered'] = astropy.table.Column( length=num_tiles, dtype=np.int32, description='Tile covered on this day number >=0 (or -1)') table['available'] = astropy.table.Column( length=num_tiles, dtype=np.int32, description='Tile available on this day number >=0 (or -1)') table['planned'] = astropy.table.Column( length=num_tiles, dtype=np.int32, description='Tile first planned on this day number >=0 (or -1)') # Add per-exposure columns. table['mjd'] = astropy.table.Column( length=num_tiles, shape=(max_exposures,), format='%.5f', description='MJD of exposure start time') table['exptime'] = astropy.table.Column( length=num_tiles, shape=(max_exposures,), format='%.1f', description='Exposure duration in seconds', unit='s') table['snr2frac'] = astropy.table.Column( length=num_tiles, shape=(max_exposures,), format='%.3f', description='Fraction of target S/N**2 ratio achieved') table['airmass'] = astropy.table.Column( length=num_tiles, shape=(max_exposures,), format='%.1f', description='Estimated airmass of observation') table['seeing'] = astropy.table.Column( length=num_tiles, shape=(max_exposures,), format='%.1f', description='Estimated FWHM seeing of observation in arcsecs', unit='arcsec') table['transparency'] = astropy.table.Column( length=num_tiles, shape=(max_exposures,), format='%.1f', description='Estimated transparency of observation') table['moonfrac'] = astropy.table.Column( length=num_tiles, shape=(max_exposures,), format='%.3f', description='Moon illuminated fraction (0-1)') table['moonalt'] = astropy.table.Column( length=num_tiles, shape=(max_exposures,), format='%.1f', description='Moon altitude angle in degrees', unit='deg') table['moonsep'] = astropy.table.Column( length=num_tiles, shape=(max_exposures,), format='%.1f', description='Moon-tile separation angle in degrees', unit='deg') # Copy tile data. table['tileid'] = tiles['TILEID'] table['pass'] = tiles['PASS'] table['ra'] = tiles['RA'] table['dec'] = tiles['DEC'] # Initialize other columns. table['status'] = 0 table['covered'] = -1 table['available'] = -1 table['planned'] = -1 table['mjd'] = 0. table['exptime'] = 0. table['snr2frac'] = 0. table['airmass'] = 0. table['seeing'] = 0. table['transparency'] = 0. else: if isinstance(restore, Progress): table = restore._table elif isinstance(restore, astropy.table.Table): table = restore else: filename = config.get_path(restore) if not os.path.exists(filename): raise ValueError('Invalid restore: {0}.'.format(restore)) table = astropy.table.Table.read(filename) self.log.info('Loaded progress from {0}.'.format(filename)) # Check that this table has the current version. if table.meta['VERSION'] != _version: raise RuntimeError( 'Progress table has incompatible version {0}.' .format(table.meta['VERSION'])) # Check that the status column matches the current min_snr2. snr2sum = table['snr2frac'].data.sum(axis=1) if not np.all(snr2sum >= 0): raise RuntimeError('Found invalid snr2frac values.') status = np.ones_like(table['status']) status[snr2sum == 0] = 0 status[snr2sum >= self.min_snr2] = 2 if not np.all(table['status'] == status): self.log.warn('Updating status values for min(SNR2) = {0:.1f}.' .format(self.min_snr2)) table['status'] = status # We could do more sanity checks here, but they shouldn't be # necessary unless the table has been modified outside this class. # Initialize attributes from table data. self._table = table mjd = table['mjd'].data observed = mjd > 0 if np.any(observed): self._num_exp = np.count_nonzero(observed) self._first_mjd = np.min(mjd[observed]) self._last_mjd = np.max(mjd[observed]) last = np.argmax(mjd.max(axis=1)) self._last_tile = self._table[last] else: self._num_exp = 0 self._first_mjd = self._last_mjd = 0. self._last_tile = None
def __init__(self, restore=None, design_hourangle=None): self.log = desiutil.log.get_logger() # Load our configuration. config = desisurvey.config.Configuration() self.min_snr2frac = config.min_snr2_fraction() GRAY = desisurvey.config.Configuration().programs.GRAY self.max_prod = GRAY.max_moon_illumination_altitude_product().to(u.deg).value self.max_frac = GRAY.max_moon_illumination() self.threshold_alt = self.max_prod / self.max_frac self.max_airmass = desisurvey.utils.cos_zenith_to_airmass(np.sin(config.min_altitude())) # Load static tile info. self.tiles = desisurvey.tiles.get_tiles() ntiles = self.tiles.ntiles # Check hourangles. if design_hourangle is None: self.design_hourangle = desisurvey.plan.load_design_hourangle() else: self.design_hourangle = np.asarray(design_hourangle) if self.design_hourangle.shape != (self.tiles.ntiles,): raise ValueError('Array design_hourangle has wrong shape.') # Initialize snr2frac, which is our only internal state. if restore is not None: # Restore the snr2frac array for a survey in progress. fullname = config.get_path(restore) if not os.path.exists(fullname): raise RuntimeError('Cannot restore scheduler from non-existent "{}".'.format(fullname)) with astropy.io.fits.open(fullname, memmap=False) as hdus: self.snr2frac = hdus[0].data.copy() if self.snr2frac.shape != (ntiles,): raise ValueError('Invalid snr2frac array shape.') self.log.debug('Restored scheduler snapshot from "{}".'.format(fullname)) else: # Initialize for a new survey. self.snr2frac = np.zeros(ntiles, float) # Initialize arrays derived from snr2frac. # Note that indexing of completed_by_pass uses tiles.pass_index, which is not necessarily # the same as range(tiles.npasses). self.completed = (self.snr2frac >= self.min_snr2frac) self.completed_by_pass = np.zeros(self.tiles.npasses, np.int32) for passnum in self.tiles.passes: idx = self.tiles.pass_index[passnum] self.completed_by_pass[idx] = np.count_nonzero(self.completed[self.tiles.passnum == passnum]) # Allocate memory for internal arrays. self.exposure_factor = np.zeros(ntiles) self.hourangle = np.zeros(ntiles) self.airmass = np.zeros(ntiles) self.in_night_pool = np.zeros(ntiles, bool) self.tile_sel = np.zeros(ntiles, bool) self.LST = 0. self.night = None # Load the ephemerides to use. self.ephem = desisurvey.ephem.get_ephem() # Initialize tile availability and priority. # No tiles will be scheduled until these are updated using update_tiles(). self.tile_available = np.zeros(self.tiles.ntiles, bool) self.tile_planned = np.zeros(self.tiles.ntiles, bool) self.tile_priority = np.zeros(self.tiles.ntiles, float) # Lookup avoidance cone angles. self.avoid_bodies = {} for body in config.avoid_bodies.keys: self.avoid_bodies[body] = getattr(config.avoid_bodies, body)().to(u.deg).value
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 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 to visualize survey scheduling and progress. """ # Set up the logger if args.debug: log = desiutil.log.get_logger(desiutil.log.DEBUG) args.verbose = True elif args.verbose: log = desiutil.log.get_logger(desiutil.log.INFO) else: log = desiutil.log.get_logger(desiutil.log.WARNING) # Freeze IERS table for consistent results. desisurvey.utils.freeze_iers() # Set the output path if requested. config = desisurvey.config.Configuration() if args.output_path is not None: config.set_output_path(args.output_path) if args.tiles_file is not None: config.tiles_file.set_value(args.tiles_file) # Look for the exposures file in the output path by default. args.exposures = config.get_path(args.exposures) # Initialize. animator = Animator(args.exposures, args.start, args.stop, args.label, args.scores) log.info('Found {0} exposures from {1} to {2} ({3} nights).'.format( animator.num_exp, args.start, args.stop, animator.num_nights)) animator.init_figure(args.nightly) if args.expid is not None: expid = animator.exposures['EXPID'] assert np.all(expid == expid[0] + np.arange(len(expid))) if (args.expid < expid[0]) or (args.expid > expid[-1]): raise RuntimeError( 'Requested exposure ID {0} not available.'.format(args.expid)) animator.draw_exposure(args.expid - expid[0], args.nightly) save_name = args.save + '.png' plt.savefig(save_name) log.info('Saved {0}.'.format(save_name)) else: nframes = animator.num_nights if args.nightly else animator.num_exp iexp = [0] def init(): return animator.artists def update(iframe): if (iframe + 1) % args.log_interval == 0: log.info('Drawing frame {0}/{1}.'.format(iframe + 1, nframes)) if args.nightly: while not animator.draw_exposure(iexp[0], nightly=True): iexp[0] += 1 else: animator.draw_exposure(iexp=iframe, nightly=False) return animator.artists log.info('Movie will be {:.1f} mins long at {:.1f} frames/sec.'.format( nframes / (60 * args.fps), args.fps)) animation = matplotlib.animation.FuncAnimation(animator.figure, update, init_func=init, blit=True, frames=nframes) writer = matplotlib.animation.writers['ffmpeg']( bitrate=2400, fps=args.fps, metadata=dict(artist='surveymovie')) save_name = args.save + '.mp4' animation.save(save_name, writer=writer, dpi=animator.dpi) log.info('Saved {0}.'.format(save_name))
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 __init__(self, name='scheduler.fits'): self.log = desiutil.log.get_logger() config = desisurvey.config.Configuration() input_file = config.get_path(name) hdus = astropy.io.fits.open(input_file, memmap=False) header = hdus[0].header self.start_date = desisurvey.utils.get_date(header['START']) self.stop_date = desisurvey.utils.get_date(header['STOP']) self.num_nights = (self.stop_date - self.start_date).days self.nside = header['NSIDE'] self.step_size = header['STEP'] * u.min self.npix = 12 * self.nside**2 self.pix_area = 360.**2 / np.pi / self.npix * u.deg**2 self.tiles = astropy.table.Table.read(input_file, hdu='TILES') self.tile_coords = astropy.coordinates.ICRS( ra=self.tiles['ra'] * u.deg, dec=self.tiles['dec'] * u.deg) self.calendar = astropy.table.Table.read(input_file, hdu='CALENDAR') self.etable = astropy.table.Table.read(input_file, hdu='EPHEM') self.t_edges = hdus['GRID'].data self.t_centers = 0.5 * (self.t_edges[1:] + self.t_edges[:-1]) self.num_times = len(self.t_centers) static = astropy.table.Table.read(input_file, hdu='STATIC') self.footprint_pixels = static['pixel'].data self.footprint = np.zeros(self.npix, bool) self.footprint[self.footprint_pixels] = True self.footprint_area = len(self.footprint_pixels) * self.pix_area self.fdust = static['dust'].data self.pixel_ra = static['ra'].data self.pixel_dec = static['dec'].data self.fexp = hdus['DYNAMIC'].data assert self.fexp.shape == (self.num_nights * self.num_times, len(self.footprint_pixels)) # Load fallback weights into a (4,3) matrix with row, column # indices 0=DARK, 1=GRAY, 2=BRIGHT, 3=DAYTIME. The row index specifies # the current program based on the observing time, and the column index # specifies the alternate fall back program. Weights are relative to 1 # for staying within the nominal program. fb = config.fallback_weights self.fallback_weights = np.zeros((4, 3)) self.fallback_weights[:3] = np.identity(3) self.fallback_weights[0, 1] = fb.gray_in_dark() self.fallback_weights[0, 2] = fb.bright_in_dark() self.fallback_weights[1, 0] = fb.dark_in_gray() self.fallback_weights[1, 2] = fb.bright_in_gray() self.fallback_weights[2, 0] = fb.dark_in_bright() self.fallback_weights[2, 1] = fb.gray_in_bright() assert np.all(self.fallback_weights >= 0) # Calculate target exposure time in seconds of each tile at nominal # conditions. self.tnom = np.empty(len(self.tiles)) for i, program in enumerate(('DARK', 'GRAY', 'BRIGHT')): sel = self.tiles['program'] == i + 1 self.tnom[sel] = getattr(config.nominal_exposure_time, program)().to(u.s).value # Initialize calculation of moon, planet positions. self.avoid_names = list(config.avoid_bodies.keys) self.moon_index = self.avoid_names.index('moon') self.avoid_ra = np.empty(len(self.avoid_names)) self.avoid_dec = np.empty(len(self.avoid_names)) self.avoid_min = np.empty(len(self.avoid_names)) for i, name in enumerate(self.avoid_names): self.avoid_min[i] = getattr(config.avoid_bodies, name)().to(u.deg).value self.last_date = None hdus.close()
def __init__(self, name='scheduler.fits'): self.log = desiutil.log.get_logger() config = desisurvey.config.Configuration() input_file = config.get_path(name) hdus = astropy.io.fits.open(input_file, memmap=False) header = hdus[0].header self.start_date = desisurvey.utils.get_date(header['START']) self.stop_date = desisurvey.utils.get_date(header['STOP']) self.num_nights = (self.stop_date - self.start_date).days self.nside = header['NSIDE'] self.step_size = header['STEP'] * u.min self.npix = 12 * self.nside ** 2 self.pix_area = 360. ** 2 / np.pi / self.npix * u.deg ** 2 self.tiles = astropy.table.Table.read(input_file, hdu='TILES') self.tile_coords = astropy.coordinates.ICRS( ra=self.tiles['ra'] * u.deg, dec=self.tiles['dec'] * u.deg) self.calendar = astropy.table.Table.read(input_file, hdu='CALENDAR') self.etable = astropy.table.Table.read(input_file, hdu='EPHEM') self.t_edges = hdus['GRID'].data self.t_centers = 0.5 * (self.t_edges[1:] + self.t_edges[:-1]) self.num_times = len(self.t_centers) static = astropy.table.Table.read(input_file, hdu='STATIC') self.footprint_pixels = static['pixel'].data self.footprint = np.zeros(self.npix, bool) self.footprint[self.footprint_pixels] = True self.footprint_area = len(self.footprint_pixels) * self.pix_area self.fdust = static['dust'].data self.pixel_ra = static['ra'].data self.pixel_dec = static['dec'].data self.fexp = hdus['DYNAMIC'].data assert self.fexp.shape == ( self.num_nights * self.num_times, len(self.footprint_pixels)) # Load fallback weights into a (4,3) matrix with row, column # indices 0=DARK, 1=GRAY, 2=BRIGHT, 3=DAYTIME. The row index specifies # the current program based on the observing time, and the column index # specifies the alternate fall back program. Weights are relative to 1 # for staying within the nominal program. fb = config.fallback_weights self.fallback_weights = np.zeros((4, 3)) self.fallback_weights[:3] = np.identity(3) self.fallback_weights[0, 1] = fb.gray_in_dark() self.fallback_weights[0, 2] = fb.bright_in_dark() self.fallback_weights[1, 0] = fb.dark_in_gray() self.fallback_weights[1, 2] = fb.bright_in_gray() self.fallback_weights[2, 0] = fb.dark_in_bright() self.fallback_weights[2, 1] = fb.gray_in_bright() assert np.all(self.fallback_weights >= 0) # Calculate target exposure time in seconds of each tile at nominal # conditions. self.tnom = np.empty(len(self.tiles)) for i, program in enumerate(('DARK', 'GRAY', 'BRIGHT')): sel = self.tiles['program'] == i + 1 self.tnom[sel] = getattr( config.nominal_exposure_time, program)().to(u.s).value # Initialize calculation of moon, planet positions. self.avoid_names = list(config.avoid_bodies.keys) self.moon_index = self.avoid_names.index('moon') self.avoid_ra = np.empty(len(self.avoid_names)) self.avoid_dec = np.empty(len(self.avoid_names)) self.avoid_min = np.empty(len(self.avoid_names)) for i, name in enumerate(self.avoid_names): self.avoid_min[i] = getattr( config.avoid_bodies, name)().to(u.deg).value self.last_date = None hdus.close()
def __init__(self, restore=None, design_hourangle=None): self.log = desiutil.log.get_logger() # Load our configuration. config = desisurvey.config.Configuration() self.min_snr2frac = config.min_snr2_fraction() GRAY = desisurvey.config.Configuration().programs.GRAY self.max_prod = GRAY.max_moon_illumination_altitude_product().to(u.deg).value self.max_frac = GRAY.max_moon_illumination() self.threshold_alt = self.max_prod / self.max_frac self.max_airmass = desisurvey.utils.cos_zenith_to_airmass(np.sin(config.min_altitude())) # Load static tile info. self.tiles = desisurvey.tiles.get_tiles() ntiles = self.tiles.ntiles # Check hourangles. if design_hourangle is None: self.design_hourangle = desisurvey.plan.load_design_hourangle() else: self.design_hourangle = np.asarray(design_hourangle) if self.design_hourangle.shape != (self.tiles.ntiles,): raise ValueError('Array design_hourangle has wrong shape.') # Initialize snr2frac, which is our only internal state. if restore is not None: # Restore the snr2frac array for a survey in progress. fullname = config.get_path(restore) if not os.path.exists(fullname): raise RuntimeError('Cannot restore scheduler from non-existent "{}".'.format(fullname)) with astropy.io.fits.open(fullname, memmap=False) as hdus: self.snr2frac = hdus[0].data.copy() if self.snr2frac.shape != (ntiles,): raise ValueError('Invalid snr2frac array shape.') self.log.debug('Restored scheduler snapshot from "{}".'.format(fullname)) else: # Initialize for a new survey. self.snr2frac = np.zeros(ntiles, float) # Initialize arrays derived from snr2frac. # Note that indexing of completed_by_pass uses tiles.pass_index, which is not necessarily # the same as range(tiles.npasses). self.completed = (self.snr2frac >= self.min_snr2frac) self.completed_by_pass = np.zeros(self.tiles.npasses, np.int32) for passnum in self.tiles.passes: idx = self.tiles.pass_index[passnum] self.completed_by_pass[idx] = np.count_nonzero(self.completed[self.tiles.passnum == passnum]) # Allocate memory for internal arrays. self.exposure_factor = np.zeros(ntiles) self.hourangle = np.zeros(ntiles) self.airmass = np.zeros(ntiles) self.in_night_pool = np.zeros(ntiles, bool) self.tile_sel = np.zeros(ntiles, bool) self.LST = 0. self.night = None # Load the ephemerides to use. self.ephem = desisurvey.ephem.get_ephem() # Initialize tile availability and priority. # No tiles will be scheduled until these are updated using update_tiles(). self.tile_available = np.zeros(self.tiles.ntiles, bool) self.tile_planned = np.zeros(self.tiles.ntiles, bool) self.tile_priority = np.zeros(self.tiles.ntiles, float) # Lookup avoidance cone angles. self.avoid_bodies = {} for body in config.avoid_bodies.keys: self.avoid_bodies[body] = getattr(config.avoid_bodies, body)().to(u.deg).value
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))