コード例 #1
0
def new_exposure(flavor, nspec=5000, night=None, expid=None, tileid=None, \
    airmass=1.0, exptime=None):
    """
    Create a new exposure and output input simulation files.
    Does not generate pixel-level simulations or noisy spectra.
    
    Args:
        nspec (optional): integer number of spectra to simulate
        night (optional): YEARMMDD string
        expid (optional): positive integer exposure ID
        tileid (optional): tile ID
        airmass (optional): airmass, default 1.0
    
    Writes:
        $DESI_SPECTRO_SIM/$PIXPROD/{night}/fibermap-{expid}.fits
        $DESI_SPECTRO_SIM/$PIXPROD/{night}/simspec-{expid}.fits
        
    Returns:
        fibermap numpy structured array
        truth dictionary
    """
    if expid is None:
        expid = get_next_expid()

    if tileid is None:
        tileid = get_next_tileid()

    if night is None:
        #- simulation obs time = now, even if sun is up
        dateobs = time.gmtime()
        night = get_night(utc=dateobs)
    else:
        #- 10pm on night YEARMMDD
        dateobs = time.strptime(night + ':22', '%Y%m%d:%H')

    params = desimodel.io.load_desiparams()
    if flavor == 'arc':
        infile = os.getenv(
            'DESI_ROOT'
        ) + '/spectro/templates/calib/v0.2/arc-lines-average.fits'
        d = fits.getdata(infile, 1)
        wave = d['AIRWAVE']
        phot = d['ELECTRONS']

        truth = dict(WAVE=wave)
        meta = None
        fibermap = desispec.io.fibermap.empty_fibermap(nspec)
        for channel in ('B', 'R', 'Z'):
            thru = desimodel.io.load_throughput(channel)
            ii = np.where((thru.wavemin <= wave) & (wave <= thru.wavemax))[0]
            truth['WAVE_' + channel] = wave[ii]
            truth['PHOT_' + channel] = np.tile(phot[ii],
                                               nspec).reshape(nspec, len(ii))

    elif flavor == 'flat':
        infile = os.getenv(
            'DESI_ROOT'
        ) + '/spectro/templates/calib/v0.2/flat-3100K-quartz-iodine.fits'
        flux = fits.getdata(infile, 0)
        hdr = fits.getheader(infile, 0)
        wave = desispec.io.util.header2wave(hdr)

        #- resample to 0.2 A grid
        dw = 0.2
        ww = np.arange(wave[0], wave[-1] + dw / 2, dw)
        flux = resample_flux(ww, wave, flux)
        wave = ww

        #- Convert to 2D for projection
        flux = np.tile(flux, nspec).reshape(nspec, len(wave))

        truth = dict(WAVE=wave, FLUX=flux)
        meta = None
        fibermap = desispec.io.fibermap.empty_fibermap(nspec)
        for channel in ('B', 'R', 'Z'):
            thru = desimodel.io.load_throughput(channel)
            ii = (thru.wavemin <= wave) & (wave <= thru.wavemax)
            phot = thru.photons(wave[ii],
                                flux[:, ii],
                                units=hdr['BUNIT'],
                                objtype='CALIB',
                                exptime=10)

            truth['WAVE_' + channel] = wave[ii]
            truth['PHOT_' + channel] = phot

    elif flavor == 'science':
        fibermap, truth = get_targets(nspec, tileid=tileid)

        flux = truth['FLUX']
        wave = truth['WAVE']
        nwave = len(wave)

        if exptime is None:
            exptime = params['exptime']

        #- Load sky [Magic knowledge of units 1e-17 erg/s/cm2/A/arcsec2]
        skyfile = os.getenv('DESIMODEL') + '/data/spectra/spec-sky.dat'
        skywave, skyflux = np.loadtxt(skyfile, unpack=True)
        skyflux = np.interp(wave, skywave, skyflux)
        truth['SKYFLUX'] = skyflux

        for channel in ('B', 'R', 'Z'):
            thru = desimodel.io.load_throughput(channel)

            ii = np.where((thru.wavemin <= wave) & (wave <= thru.wavemax))[0]

            #- Project flux to photons
            phot = thru.photons(wave[ii],
                                flux[:, ii],
                                units=truth['UNITS'],
                                objtype=truth['OBJTYPE'],
                                exptime=exptime,
                                airmass=airmass)

            truth['PHOT_' + channel] = phot
            truth['WAVE_' + channel] = wave[ii]

            #- Project sky flux to photons
            skyphot = thru.photons(wave[ii],
                                   skyflux[ii] * airmass,
                                   units='1e-17 erg/s/cm2/A/arcsec2',
                                   objtype='SKY',
                                   exptime=exptime,
                                   airmass=airmass)

            #- 2D version
            ### truth['SKYPHOT_'+channel] = np.tile(skyphot, nspec).reshape((nspec, len(ii)))
            #- 1D version
            truth['SKYPHOT_' + channel] = skyphot.astype(np.float32)

        #- NOTE: someday skyflux and skyphot may be 2D instead of 1D

        #- Extract the metadata part of the truth dictionary into a table
        columns = (
            'OBJTYPE',
            'REDSHIFT',
            'TEMPLATEID',
            'D4000',
            'OIIFLUX',
            'VDISP',
        )
        meta = {key: truth[key] for key in columns}

    #- (end indentation for arc/flat/science flavors)

    #- Override $DESI_SPECTRO_DATA in order to write to simulation area
    datadir_orig = os.getenv('DESI_SPECTRO_DATA')
    simbase = os.path.join(os.getenv('DESI_SPECTRO_SIM'), os.getenv('PIXPROD'))
    os.environ['DESI_SPECTRO_DATA'] = simbase

    #- Write fibermap
    telera, teledec = io.get_tile_radec(tileid)
    hdr = dict(
        NIGHT=(night, 'Night of observation YEARMMDD'),
        EXPID=(expid, 'DESI exposure ID'),
        TILEID=(tileid, 'DESI tile ID'),
        FLAVOR=(flavor, 'Flavor [arc, flat, science, ...]'),
        TELRA=(telera, 'Telescope pointing RA [degrees]'),
        TELDEC=(teledec, 'Telescope pointing dec [degrees]'),
    )
    #- ISO 8601 DATE-OBS year-mm-ddThh:mm:ss
    fiberfile = desispec.io.findfile('fibermap', night, expid)
    desispec.io.write_fibermap(fiberfile, fibermap, header=hdr)
    print fiberfile

    #- Write simspec; expand fibermap header
    hdr['AIRMASS'] = (airmass, 'Airmass at middle of exposure')
    hdr['EXPTIME'] = (exptime, 'Exposure time [sec]')
    hdr['DATE-OBS'] = (time.strftime('%FT%T', dateobs), 'Start of exposure')

    simfile = io.write_simspec(meta, truth, expid, night, header=hdr)
    print(simfile)

    #- Update obslog that we succeeded with this exposure
    update_obslog(flavor, expid, dateobs, tileid)

    #- Restore $DESI_SPECTRO_DATA
    if datadir_orig is not None:
        os.environ['DESI_SPECTRO_DATA'] = datadir_orig
    else:
        del os.environ['DESI_SPECTRO_DATA']

    return fibermap, truth
コード例 #2
0
def new_exposure(program,
                 nspec=5000,
                 night=None,
                 expid=None,
                 tileid=None,
                 nproc=None,
                 seed=None,
                 obsconditions=None,
                 specify_targets=dict(),
                 testslit=False,
                 exptime=None,
                 arc_lines_filename=None,
                 flat_spectrum_filename=None,
                 outdir=None,
                 config='desi',
                 telescope=None,
                 overwrite=False):
    """
    Create a new exposure and output input simulation files.
    Does not generate pixel-level simulations or noisy spectra.

    Args:
        program (str): 'arc', 'flat', 'bright', 'dark', 'bgs', 'mws', ...
        nspec (int, optional): number of spectra to simulate
        night (str, optional): YEARMMDD string
        expid (int, optional): positive integer exposure ID
        tileid (int, optional): integer tile ID
        nproc (object, optional): What does this do?
        seed (int, optional): random seed
        obsconditions (str or dict-like, optional): see options below
        specify_targets (dict of dicts, optional): Define target properties like magnitude and redshift
            for each target class. Each objtype has its own key,value pair
            see simspec.templates.specify_galparams_dict()
            or simsepc.templates.specify_starparams_dict()
        testslit (bool, optional): simulate test slit if True, default False; only for arc/flat
        exptime (float, optional): exposure time [seconds], overrides obsconditions['EXPTIME']
        arc_lines_filename (str, optional): use alternate arc lines filename (used if program="arc")
        flat_spectrum_filename (str, optional): use alternate flat spectrum filename (used if program="flat")
        outdir (str, optional): output directory
        config (str, optional): the yaml configuration to load
        telescope (str, optional): the telescope used (i.e. 1m, 160mm)
        overwrite (bool, optional): optionally clobber existing files

    Returns:
        science: sim, fibermap, meta, obsconditions, objmeta

    Writes to outdir or $DESI_SPECTRO_SIM/$PIXPROD/{night}/

        * fibermap-{expid}.fits
        * simspec-{expid}.fits

    input obsconditions can be a string 'dark', 'gray', 'bright', or dict-like
    observation metadata with keys SEEING (arcsec), EXPTIME (sec), AIRMASS,
    MOONFRAC (0-1), MOONALT (deg), MOONSEP (deg).  Output obsconditions is
    is expanded dict-like structure.

    program is used to pick the sky brightness, and is propagated to
    desisim.targets.sample_objtype() to get the correct distribution of
    targets for a given program, e.g. ELGs, LRGs, QSOs for program='dark'.

    if program is 'arc' or 'flat', then `sim` is truth table with keys
    FLUX and WAVE; and meta=None and obsconditions=None.

    Also see simexp.simarc(), .simflat(), and .simscience(), the last of
    which simulates a science exposure given surveysim obsconditions input,
    fiber assignments, and pre-generated mock target spectra.
    """
    if expid is None:
        expid = get_next_expid()

    if tileid is None:
        tileid = get_next_tileid()

    if night is None:
        #- simulation obs time = now, even if sun is up
        dateobs = time.gmtime()
        night = get_night(utc=dateobs)
    else:
        #- 10pm on night YEARMMDD
        night = str(night)  #- just in case we got an integer instead of string
        dateobs = time.strptime(night + ':22', '%Y%m%d:%H')

    outsimspec = desisim.io.findfile('simspec', night, expid)
    outfibermap = desisim.io.findfile('simfibermap', night, expid)

    if outdir is not None:
        outsimspec = os.path.join(outdir, os.path.basename(outsimspec))
        outfibermap = os.path.join(outdir, os.path.basename(outfibermap))

    program = program.lower()
    log.debug('Generating {} targets'.format(nspec))

    header = dict(NIGHT=night, EXPID=expid, PROGRAM=program)
    if program in ('arc', 'flat'):
        header['FLAVOR'] = program
    else:
        header['FLAVOR'] = 'science'

    #- ISO 8601 DATE-OBS year-mm-ddThh:mm:ss
    header['DATE-OBS'] = time.strftime('%FT%T', dateobs)

    if program == 'arc':
        if arc_lines_filename is None:
            infile = os.getenv(
                'DESI_ROOT'
            ) + '/spectro/templates/calib/v0.4/arc-lines-average-in-vacuum-from-winlight-20170118.fits'
        else:
            infile = arc_lines_filename
        arcdata = fits.getdata(infile, 1)
        if exptime is None:
            exptime = 5
        wave, phot, fibermap = desisim.simexp.simarc(arcdata,
                                                     nspec=nspec,
                                                     testslit=testslit)

        header['EXPTIME'] = exptime
        desisim.io.write_simspec_arc(outsimspec,
                                     wave,
                                     phot,
                                     header,
                                     fibermap=fibermap,
                                     overwrite=overwrite)

        fibermap.meta['NIGHT'] = night
        fibermap.meta['EXPID'] = expid
        desispec.io.write_fibermap(outfibermap, fibermap)
        truth = dict(WAVE=wave, PHOT=phot, UNITS='photon')
        return truth, fibermap, None, None, None

    elif program == 'flat':
        if flat_spectrum_filename is None:
            infile = os.getenv(
                'DESI_ROOT'
            ) + '/spectro/templates/calib/v0.4/flat-3100K-quartz-iodine.fits'
        else:
            infile = flat_spectrum_filename

        if exptime is None:
            exptime = 10
        sim, fibermap = desisim.simexp.simflat(infile,
                                               nspec=nspec,
                                               exptime=exptime,
                                               testslit=testslit,
                                               psfconvolve=False)

        header['EXPTIME'] = exptime
        header['FLAVOR'] = 'flat'
        desisim.io.write_simspec(sim,
                                 truth=None,
                                 fibermap=fibermap,
                                 obs=None,
                                 expid=expid,
                                 night=night,
                                 header=header,
                                 filename=outsimspec,
                                 overwrite=overwrite)

        fibermap.meta['NIGHT'] = night
        fibermap.meta['EXPID'] = expid
        desispec.io.write_fibermap(outfibermap, fibermap)
        # fluxunits = 1e-17 * u.erg / (u.s * u.cm**2 * u.Angstrom)
        fluxunits = '1e-17 erg/(s * cm2 * Angstrom)'
        flux = sim.simulated['source_flux'].to(fluxunits)
        wave = sim.simulated['wavelength'].to('Angstrom')
        truth = dict(WAVE=wave, FLUX=flux, UNITS=str(fluxunits))
        return truth, fibermap, None, None, None

    #- all other programs
    fibermap, (flux, wave, meta,
               objmeta) = get_targets_parallel(nspec,
                                               program,
                                               tileid=tileid,
                                               nproc=nproc,
                                               seed=seed,
                                               specify_targets=specify_targets,
                                               config=config,
                                               telescope=telescope)

    if obsconditions is None:
        if program in ['dark', 'lrg', 'qso']:
            obsconditions = desisim.simexp.reference_conditions['DARK']
        elif program in ['elg', 'gray', 'grey']:
            obsconditions = desisim.simexp.reference_conditions['GRAY']
        elif program in ['mws', 'bgs', 'bright']:
            obsconditions = desisim.simexp.reference_conditions['BRIGHT']
        else:
            raise ValueError('unknown program {}'.format(program))
    elif isinstance(obsconditions, str):
        try:
            obsconditions = desisim.simexp.reference_conditions[
                obsconditions.upper()]
        except KeyError:
            raise ValueError('obsconditions {} not in {}'.format(
                obsconditions.upper(),
                list(desisim.simexp.reference_conditions.keys())))

    if exptime is not None:
        obsconditions['EXPTIME'] = exptime

    desiparams = load_desiparams(config=config, telescope=telescope)
    sim = simulate_spectra(wave,
                           flux,
                           fibermap=fibermap,
                           obsconditions=obsconditions,
                           psfconvolve=False,
                           specsim_config_file=config,
                           params=desiparams)

    #- Write fibermap
    telera, teledec = io.get_tile_radec(tileid)
    hdr = dict(
        NIGHT=(night, 'Night of observation YEARMMDD'),
        EXPID=(expid, 'DESI exposure ID'),
        TILEID=(tileid, 'DESI tile ID'),
        PROGRAM=(program, 'program [dark, bright, ...]'),
        FLAVOR=('science', 'Flavor [arc, flat, science, zero, ...]'),
        TELRA=(telera, 'Telescope pointing RA [degrees]'),
        TELDEC=(teledec, 'Telescope pointing dec [degrees]'),
        AIRMASS=(obsconditions['AIRMASS'], 'Airmass at middle of exposure'),
        EXPTIME=(obsconditions['EXPTIME'], 'Exposure time [sec]'),
        SEEING=(obsconditions['SEEING'], 'Seeing FWHM [arcsec]'),
        MOONFRAC=(obsconditions['MOONFRAC'],
                  'Moon illumination fraction 0-1; 1=full'),
        MOONALT=(obsconditions['MOONALT'], 'Moon altitude [degrees]'),
        MOONSEP=(obsconditions['MOONSEP'],
                 'Moon:tile separation angle [degrees]'),
    )
    hdr['DATE-OBS'] = (time.strftime('%FT%T', dateobs), 'Start of exposure')

    simfile = io.write_simspec(sim,
                               meta,
                               fibermap,
                               obsconditions,
                               expid,
                               night,
                               objmeta=objmeta,
                               header=hdr,
                               filename=outsimspec,
                               overwrite=overwrite)

    if not isinstance(fibermap, table.Table):
        fibermap = table.Table(fibermap)

    fibermap.meta.update(hdr)
    desispec.io.write_fibermap(outfibermap, fibermap)
    log.info('Wrote ' + outfibermap)

    update_obslog(obstype='science',
                  program=program,
                  expid=expid,
                  dateobs=dateobs,
                  tileid=tileid)

    return sim, fibermap, meta, obsconditions, objmeta
コード例 #3
0
ファイル: obs.py プロジェクト: forero/desisim
def new_exposure(flavor, nspec=5000, night=None, expid=None, tileid=None, airmass=1.0, \
    exptime=None):
    """
    Create a new exposure and output input simulation files.
    Does not generate pixel-level simulations or noisy spectra.
    
    Args:
        nspec (optional): integer number of spectra to simulate
        night (optional): YEARMMDD string
        expid (optional): positive integer exposure ID
        tileid (optional): tile ID
        airmass (optional): airmass, default 1.0
    
    Writes:
        $DESI_SPECTRO_SIM/$PIXPROD/{night}/fibermap-{expid}.fits
        $DESI_SPECTRO_SIM/$PIXPROD/{night}/simspec-{expid}.fits
        
    Returns:
        fibermap numpy structured array
        truth dictionary
    """
    if expid is None:
        expid = get_next_expid()
    
    if tileid is None:
        tileid = get_next_tileid()

    if night is None:
        #- simulation obs time = now, even if sun is up
        dateobs = time.gmtime()
        night = get_night(utc=dateobs)
    else:
        #- 10pm on night YEARMMDD
        dateobs = time.strptime(night+':22', '%Y%m%d:%H')
    
    params = desimodel.io.load_desiparams()    
    if flavor == 'arc':
        infile = os.getenv('DESI_ROOT')+'/spectro/templates/calib/v0.1/arc-lines-average.fits'
        d = fits.getdata(infile, 1)
        wave = d['AIRWAVE']
        phot = d['ELECTRONS']
        
        truth = dict(WAVE=wave)
        meta = None
        fibermap = desispec.io.fibermap.empty_fibermap(nspec)
        for channel in ('B', 'R', 'Z'):
            thru = desimodel.io.load_throughput(channel)        
            ii = np.where( (thru.wavemin <= wave) & (wave <= thru.wavemax) )[0]
            truth['WAVE_'+channel] = wave[ii]
            truth['PHOT_'+channel] = np.tile(phot[ii], nspec).reshape(nspec, len(ii))

    elif flavor == 'flat':
        infile = os.getenv('DESI_ROOT')+'/spectro/templates/calib/v0.1/flat-3100K-quartz-iodine.fits'
        flux = fits.getdata(infile, 0)
        hdr = fits.getheader(infile, 0)
        wave = desispec.io.util.header2wave(hdr)

        #- resample to 0.2 A grid
        dw = 0.2
        ww = np.arange(wave[0], wave[-1]+dw/2, dw)
        flux = resample_flux(ww, wave, flux)
        wave = ww

        #- Convert to 2D for projection
        flux = np.tile(flux, nspec).reshape(nspec, len(wave))

        truth = dict(WAVE=wave, FLUX=flux)
        meta = None
        fibermap = desispec.io.fibermap.empty_fibermap(nspec)
        for channel in ('B', 'R', 'Z'):
            psf = desimodel.io.load_psf(channel)
            thru = desimodel.io.load_throughput(channel)
            ii = (psf.wmin <= wave) & (wave <= psf.wmax)
            phot = thru.photons(wave[ii], flux[:,ii], units=hdr['BUNIT'], objtype='CALIB')
        
            truth['WAVE_'+channel] = wave[ii]
            truth['PHOT_'+channel] = phot
        
    elif flavor == 'science':
        fibermap, truth = get_targets(nspec, tileid=tileid)
            
        flux = truth['FLUX']
        wave = truth['WAVE']
        nwave = len(wave)
    
        if exptime is None:
            exptime = params['exptime']
    
        #- Load sky [Magic knowledge of units 1e-17 erg/s/cm2/A/arcsec2]
        skyfile = os.getenv('DESIMODEL')+'/data/spectra/spec-sky.dat'
        skywave, skyflux = np.loadtxt(skyfile, unpack=True)
        skyflux = np.interp(wave, skywave, skyflux)
        truth['SKYFLUX'] = skyflux

        for channel in ('B', 'R', 'Z'):
            thru = desimodel.io.load_throughput(channel)
        
            ii = np.where( (thru.wavemin <= wave) & (wave <= thru.wavemax) )[0]
        
            #- Project flux to photons
            phot = thru.photons(wave[ii], flux[:,ii], units='1e-17 erg/s/cm2/A',
                    objtype=truth['OBJTYPE'], exptime=exptime,
                    airmass=airmass)
                
            truth['PHOT_'+channel] = phot
            truth['WAVE_'+channel] = wave[ii]
    
            #- Project sky flux to photons
            skyphot = thru.photons(wave[ii], skyflux[ii]*airmass,
                units='1e-17 erg/s/cm2/A/arcsec2',
                objtype='SKY', exptime=exptime, airmass=airmass)
    
            #- 2D version
            ### truth['SKYPHOT_'+channel] = np.tile(skyphot, nspec).reshape((nspec, len(ii)))
            #- 1D version
            truth['SKYPHOT_'+channel] = skyphot.astype(np.float32)
        
        #- NOTE: someday skyflux and skyphot may be 2D instead of 1D
        
        #- Extract the metadata part of the truth dictionary into a table
        columns = (
            'OBJTYPE',
            'REDSHIFT',
            'TEMPLATEID',
            'O2FLUX',
        )
        meta = _dict2ndarray(truth, columns)
        
    #- (end indentation for arc/flat/science flavors)
        
    #- Write fibermap
    telera, teledec = io.get_tile_radec(tileid)
    hdr = dict(
        NIGHT = (night, 'Night of observation YEARMMDD'),
        EXPID = (expid, 'DESI exposure ID'),
        TILEID = (tileid, 'DESI tile ID'),
        FLAVOR = (flavor, 'Flavor [arc, flat, science, ...]'),
        TELERA = (telera, 'Telescope pointing RA [degrees]'),
        TELEDEC = (teledec, 'Telescope pointing dec [degrees]'),
        )
    fiberfile = desispec.io.findfile('fibermap', night, expid)
    desispec.io.write_fibermap(fiberfile, fibermap, header=hdr)
    print fiberfile
    
    #- Write simfile
    hdr = dict(
        AIRMASS=(airmass, 'Airmass at middle of exposure'),
        EXPTIME=(exptime, 'Exposure time [sec]'),
        FLAVOR=(flavor, 'exposure flavor [arc, flat, science]'),
        )
    simfile = io.write_simspec(meta, truth, expid, night, header=hdr)
    print simfile

    #- Update obslog that we succeeded with this exposure
    update_obslog(flavor, expid, dateobs, tileid)
    
    return fibermap, truth
コード例 #4
0
ファイル: obs.py プロジェクト: desihub/desisim
def new_exposure(program, nspec=5000, night=None, expid=None, tileid=None,
                 nproc=None, seed=None, obsconditions=None,
                 specify_targets=dict(), testslit=False, exptime=None,
                 arc_lines_filename=None, flat_spectrum_filename=None,
                 outdir=None, overwrite=False):
    """
    Create a new exposure and output input simulation files.
    Does not generate pixel-level simulations or noisy spectra.

    Args:
        program (str): 'arc', 'flat', 'bright', 'dark', 'bgs', 'mws', ...
        nspec (int, optional): number of spectra to simulate
        night (str, optional): YEARMMDD string
        expid (int, optional): positive integer exposure ID
        tileid (int, optional): integer tile ID
        nproc (object, optional): What does this do?
        seed (int, optional): random seed
        obsconditions (str or dict-like, optional): see options below
        specify_targets (dict of dicts, optional): Define target properties like magnitude and redshift
            for each target class. Each objtype has its own key,value pair
            see simspec.templates.specify_galparams_dict()
            or simsepc.templates.specify_starparams_dict()
        testslit (bool, optional): simulate test slit if True, default False; only for arc/flat
        exptime (float, optional): exposure time [seconds], overrides obsconditions['EXPTIME']
        arc_lines_filename (str, optional): use alternate arc lines filename (used if program="arc")
        flat_spectrum_filename (str, optional): use alternate flat spectrum filename (used if program="flat")
        outdir (str, optional): output directory
        overwrite (bool, optional): optionally clobber existing files

    Returns:
        science: sim, fibermap, meta, obsconditions, objmeta

    Writes to outdir or $DESI_SPECTRO_SIM/$PIXPROD/{night}/

        * fibermap-{expid}.fits
        * simspec-{expid}.fits

    input obsconditions can be a string 'dark', 'gray', 'bright', or dict-like
    observation metadata with keys SEEING (arcsec), EXPTIME (sec), AIRMASS,
    MOONFRAC (0-1), MOONALT (deg), MOONSEP (deg).  Output obsconditions is
    is expanded dict-like structure.

    program is used to pick the sky brightness, and is propagated to
    desisim.targets.sample_objtype() to get the correct distribution of
    targets for a given program, e.g. ELGs, LRGs, QSOs for program='dark'.

    if program is 'arc' or 'flat', then `sim` is truth table with keys
    FLUX and WAVE; and meta=None and obsconditions=None.

    Also see simexp.simarc(), .simflat(), and .simscience(), the last of
    which simulates a science exposure given surveysim obsconditions input,
    fiber assignments, and pre-generated mock target spectra.
    """
    if expid is None:
        expid = get_next_expid()

    if tileid is None:
        tileid = get_next_tileid()

    if night is None:
        #- simulation obs time = now, even if sun is up
        dateobs = time.gmtime()
        night = get_night(utc=dateobs)
    else:
        #- 10pm on night YEARMMDD
        night = str(night)  #- just in case we got an integer instead of string
        dateobs = time.strptime(night+':22', '%Y%m%d:%H')

    outsimspec = desisim.io.findfile('simspec', night, expid)
    outfibermap = desisim.io.findfile('simfibermap', night, expid)

    if outdir is not None:
        outsimspec = os.path.join(outdir, os.path.basename(outsimspec))
        outfibermap = os.path.join(outdir, os.path.basename(outfibermap))

    program = program.lower()
    log.debug('Generating {} targets'.format(nspec))

    header = dict(NIGHT=night, EXPID=expid, PROGRAM=program)
    if program in ('arc', 'flat'):
        header['FLAVOR'] = program
    else:
        header['FLAVOR'] = 'science'

    #- ISO 8601 DATE-OBS year-mm-ddThh:mm:ss
    header['DATE-OBS'] = time.strftime('%FT%T', dateobs)

    if program == 'arc':
        if arc_lines_filename is None :
            infile = os.getenv('DESI_ROOT')+'/spectro/templates/calib/v0.4/arc-lines-average-in-vacuum-from-winlight-20170118.fits'
        else :
            infile = arc_lines_filename
        arcdata = fits.getdata(infile, 1)
        if exptime is None:
            exptime = 5
        wave, phot, fibermap = desisim.simexp.simarc(arcdata, nspec=nspec, testslit=testslit)

        header['EXPTIME'] = exptime
        desisim.io.write_simspec_arc(outsimspec, wave, phot, header, fibermap=fibermap, overwrite=overwrite)

        fibermap.meta['NIGHT'] = night
        fibermap.meta['EXPID'] = expid
        desispec.io.write_fibermap(outfibermap, fibermap)
        truth = dict(WAVE=wave, PHOT=phot, UNITS='photon')
        return truth, fibermap, None, None, None

    elif program == 'flat':
        if flat_spectrum_filename is None :
            infile = os.getenv('DESI_ROOT')+'/spectro/templates/calib/v0.4/flat-3100K-quartz-iodine.fits'
        else :
            infile = flat_spectrum_filename

        if exptime is None:
            exptime = 10
        sim, fibermap = desisim.simexp.simflat(infile, nspec=nspec,
            exptime=exptime, testslit=testslit, psfconvolve=False)

        header['EXPTIME'] = exptime
        header['FLAVOR'] = 'flat'
        desisim.io.write_simspec(sim, truth=None, fibermap=fibermap, obs=None,
            expid=expid, night=night, header=header, filename=outsimspec, overwrite=overwrite)

        fibermap.meta['NIGHT'] = night
        fibermap.meta['EXPID'] = expid
        desispec.io.write_fibermap(outfibermap, fibermap)
        # fluxunits = 1e-17 * u.erg / (u.s * u.cm**2 * u.Angstrom)
        fluxunits = '1e-17 erg/(s * cm2 * Angstrom)'
        flux = sim.simulated['source_flux'].to(fluxunits)
        wave = sim.simulated['wavelength'].to('Angstrom')
        truth = dict(WAVE=wave, FLUX=flux, UNITS=str(fluxunits))
        return truth, fibermap, None, None, None

    #- all other programs
    fibermap, (flux, wave, meta, objmeta) = get_targets_parallel(nspec, program,
        tileid=tileid, nproc=nproc, seed=seed, specify_targets=specify_targets)

    if obsconditions is None:
        if program in ['dark', 'lrg', 'qso']:
            obsconditions = desisim.simexp.reference_conditions['DARK']
        elif program in ['elg', 'gray', 'grey']:
            obsconditions = desisim.simexp.reference_conditions['GRAY']
        elif program in ['mws', 'bgs', 'bright']:
            obsconditions = desisim.simexp.reference_conditions['BRIGHT']
        else:
            raise ValueError('unknown program {}'.format(program))
    elif isinstance(obsconditions, str):
        try:
            obsconditions = desisim.simexp.reference_conditions[obsconditions.upper()]
        except KeyError:
            raise ValueError('obsconditions {} not in {}'.format(
                obsconditions.upper(),
                list(desisim.simexp.reference_conditions.keys())))

    if exptime is not None:
        obsconditions['EXPTIME'] = exptime

    sim = simulate_spectra(wave, flux, fibermap=fibermap,
        obsconditions=obsconditions, psfconvolve=False)

    #- Write fibermap
    telera, teledec = io.get_tile_radec(tileid)
    hdr = dict(
        NIGHT = (night, 'Night of observation YEARMMDD'),
        EXPID = (expid, 'DESI exposure ID'),
        TILEID = (tileid, 'DESI tile ID'),
        PROGRAM = (program, 'program [dark, bright, ...]'),
        FLAVOR = ('science', 'Flavor [arc, flat, science, zero, ...]'),
        TELRA = (telera, 'Telescope pointing RA [degrees]'),
        TELDEC = (teledec, 'Telescope pointing dec [degrees]'),
        AIRMASS = (obsconditions['AIRMASS'], 'Airmass at middle of exposure'),
        EXPTIME = (obsconditions['EXPTIME'], 'Exposure time [sec]'),
        SEEING = (obsconditions['SEEING'], 'Seeing FWHM [arcsec]'),
        MOONFRAC = (obsconditions['MOONFRAC'], 'Moon illumination fraction 0-1; 1=full'),
        MOONALT  = (obsconditions['MOONALT'], 'Moon altitude [degrees]'),
        MOONSEP  = (obsconditions['MOONSEP'], 'Moon:tile separation angle [degrees]'),
        )
    hdr['DATE-OBS'] = (time.strftime('%FT%T', dateobs), 'Start of exposure')

    simfile = io.write_simspec(sim, meta, fibermap, obsconditions,
                               expid, night, objmeta=objmeta, header=hdr,
                               filename=outsimspec, overwrite=overwrite)

    if not isinstance(fibermap, table.Table):
        fibermap = table.Table(fibermap)

    fibermap.meta.update(hdr)
    desispec.io.write_fibermap(outfibermap, fibermap)
    log.info('Wrote '+outfibermap)

    update_obslog(obstype='science', program=program, expid=expid, dateobs=dateobs, tileid=tileid)

    return sim, fibermap, meta, obsconditions, objmeta