Example #1
0
    def run(self, indir):
        '''TODO: document'''

        log = desiutil.log.get_logger()

        results = list()

        infiles = glob.glob(os.path.join(indir, 'qframe-*.fits'))
        if len(infiles) == 0 :
            log.error("no qframe in {}".format(indir))
            return None
    
        for filename in infiles:
            qframe = read_qframe(filename)
            night = int(qframe.meta['NIGHT'])
            expid = int(qframe.meta['EXPID'])
            cam = qframe.meta['CAMERA'][0].upper()
            spectro = int(qframe.meta['CAMERA'][1])

            log.debug("computing scores for {} {} {} {}".format(night,expid,cam,spectro))

            scores,comments = compute_frame_scores(qframe,suffix="RAW",flux_per_angstrom=True)
            
            has_calib_frame = False
            cfilename=filename.replace("qframe","qcframe")
            if os.path.isfile(cfilename) : # add scores of calibrated sky-subtracted frame
                qcframe = read_qframe(cfilename)
                cscores,comments = compute_frame_scores(qcframe,suffix="CALIB",flux_per_angstrom=True)
                for k in cscores.keys() :
                    scores[k] = cscores[k]
                has_calib_frame = True

            nfibers=scores['INTEG_RAW_FLUX_'+cam].size
            for f in range(nfibers) :
                fiber = int(qframe.fibermap["FIBER"][f])
                if has_calib_frame :
                    results.append(collections.OrderedDict(
                        NIGHT=night, EXPID=expid, SPECTRO=spectro, CAM=cam, FIBER=fiber,
                        INTEG_RAW_FLUX=scores['INTEG_RAW_FLUX_'+cam][f],
                        MEDIAN_RAW_FLUX=scores['MEDIAN_RAW_FLUX_'+cam][f],
                        MEDIAN_RAW_SNR=scores['MEDIAN_RAW_SNR_'+cam][f],
                        INTEG_CALIB_FLUX=scores['INTEG_CALIB_FLUX_'+cam][f],
                        MEDIAN_CALIB_FLUX=scores['MEDIAN_CALIB_FLUX_'+cam][f],
                        MEDIAN_CALIB_SNR=scores['MEDIAN_CALIB_SNR_'+cam][f]))
                else :
                    results.append(collections.OrderedDict(
                        NIGHT=night, EXPID=expid, SPECTRO=spectro, CAM=cam, FIBER=fiber,
                        INTEG_RAW_FLUX=scores['INTEG_RAW_FLUX_'+cam][f],
                        MEDIAN_RAW_FLUX=scores['MEDIAN_RAW_FLUX_'+cam][f],
                        MEDIAN_RAW_SNR=scores['MEDIAN_RAW_SNR_'+cam][f]))
        if len(results)==0 :
            return None
        return Table(results, names=results[0].keys())
Example #2
0
    def run(self, indir):
        '''TODO: document'''

        log = desiutil.log.get_logger()

        results = list()

        infiles = glob.glob(os.path.join(indir, 'qframe-*.fits'))
        if len(infiles) == 0:
            log.error("no qframe in {}".format(indir))
            return None

        for filename in infiles:
            qframe = read_qframe(filename)
            night = int(qframe.meta['NIGHT'])
            expid = int(qframe.meta['EXPID'])
            cam = qframe.meta['CAMERA'][0].upper()
            spectro = int(qframe.meta['CAMERA'][1])

            try:
                cfinder = CalibFinder([qframe.meta])
            except:
                log.error(
                    "failed to find calib for qframe {}".format(filename))
                continue
            if not cfinder.haskey("FIBERFLAT"):
                log.warning(
                    "no known fiberflat for qframe {}".format(filename))
                continue
            fflat = read_fiberflat(cfinder.findfile("FIBERFLAT"))
            tmp = np.median(fflat.fiberflat, axis=1)
            reference_fflat = tmp / np.median(tmp)

            tmp = np.median(qframe.flux, axis=1)
            this_fflat = tmp / np.median(tmp)

            for f, fiber in enumerate(qframe.fibermap["FIBER"]):
                results.append(
                    collections.OrderedDict(NIGHT=night,
                                            EXPID=expid,
                                            SPECTRO=spectro,
                                            CAM=cam,
                                            FIBER=fiber,
                                            FIBERFLAT=this_fflat[f],
                                            REF_FIBERFLAT=reference_fflat[f]))

        if len(results) == 0:
            return None
        return Table(results, names=results[0].keys())
Example #3
0
    def run(self, indir):
        '''TODO: document'''
        log = desiutil.log.get_logger()
        infiles = glob.glob(os.path.join(indir, 'psf-*.fits'))
        if len(infiles) == 0:
            log.error('No {}/psf*.fits files found'.format(indir))
            return None

        results = list()
        for filename in infiles:
            log.debug(filename)
            hdr = fitsio.read_header(filename)
            night = hdr['NIGHT']
            expid = hdr['EXPID']
            cam = hdr['CAMERA'][0].upper()
            spectro = int(hdr['CAMERA'][1])
            dico={"NIGHT":night,"EXPID":expid,"SPECTRO":spectro,"CAM":cam}
            for k in ["MEANDX","MINDX","MAXDX","MEANDY","MINDY","MAXDY"] :
                dico[k]=hdr[k]
            log.debug("{} {} {} {}".format(night,expid,cam,spectro))
            results.append(collections.OrderedDict(**dico))
        return Table(results, names=results[0].keys())
Example #4
0
def write_tables(indir, outdir, expnights=None):
    '''
    Parses directory for available nights, exposures to generate
    nights and exposures tables
    
    Args:
        indir : directory of nights
        outdir : directory where to write nights table

    Options:
        expnights (list) : only update exposures tables for these nights
    '''
    import re
    from astropy.table import Table
    from nightwatch.webpages import tables as web_tables
    from pkg_resources import resource_filename
    from shutil import copyfile
    from collections import Counter

    log = desiutil.log.get_logger()
    log.info(f'Tabulating exposures in {indir}')

    #- Count night/expid directories to get num exp per night
    expdirs = sorted(glob.glob(f"{indir}/20*/[0-9]*"))
    nights = list()
    re_expid = re.compile('^\d{8}$')
    re_night = re.compile('^20\d{6}$')
    for expdir in expdirs:
        expid = os.path.basename(expdir)
        night = os.path.basename(os.path.dirname(expdir))
        if re_expid.match(expid) and re_night.match(night):
            nights.append(night)

    num_exp_per_night = Counter(nights)

    #- Build the exposures table for the requested nights
    rows = list()
    for expdir in expdirs:
        expid = os.path.basename(expdir)
        night = os.path.basename(os.path.dirname(expdir))
        if re_expid.match(expid) and re_night.match(night) and \
           (expnights is None or int(night) in expnights):

            night = int(night)
            expid = int(expid)

            qafile = os.path.join(expdir, 'qa-{:08d}.fits'.format(expid))

            #- gets the list of failed qprocs for each expid
            expfiles = os.listdir(expdir)
            preproc_cams = [
                i.split("-")[1] for i in expfiles
                if re.match(r'preproc-.*-.*.fits', i)
            ]
            log_cams = [
                i.split("-")[1] for i in expfiles if re.match(r'.*\.log', i)
            ]
            qfails = [i for i in log_cams if i not in preproc_cams]

            if os.path.exists(qafile):
                try:
                    with fitsio.FITS(qafile) as fits:
                        qproc_status = fits['QPROC_STATUS'].read()
                        exitcode = np.count_nonzero(qproc_status['QPROC_EXIT'])
                except IOError:
                    exitcode = 0

                rows.append(
                    dict(NIGHT=night,
                         EXPID=expid,
                         FAIL=0,
                         QPROC=qfails,
                         QPROC_EXIT=exitcode))
            else:
                log.error('Missing {}'.format(qafile))
                rows.append(
                    dict(NIGHT=night,
                         EXPID=expid,
                         FAIL=1,
                         QPROC=None,
                         QPROC_EXIT=None))

    if len(rows) == 0:
        msg = "No exp dirs found in {}/NIGHT/EXPID".format(indir)
        raise RuntimeError(msg)

    exposures = Table(rows)

    caldir = os.path.join(outdir, 'static')
    if not os.path.isdir(caldir):
        os.makedirs(caldir)

    files = [
        'bootstrap.js', 'bootstrap.css', 'bootstrap-year-calendar.css',
        'bootstrap-year-calendar.js', 'jquery_min.js', 'popper_min.js',
        'live.js'
    ]
    for f in files:
        outfile = os.path.join(outdir, 'static', f)
        if not os.path.exists(outfile):
            infile = resource_filename('nightwatch', os.path.join('static', f))
            copyfile(infile, outfile)

    nightsfile = os.path.join(outdir, 'nights.html')
    web_tables.write_calendar(nightsfile, num_exp_per_night)

    web_tables.write_exposures_tables(indir,
                                      outdir,
                                      exposures,
                                      nights=expnights)
Example #5
0
def run_qproc(rawfile, outdir, ncpu=None, cameras=None):
    '''
    Determine the obstype of the rawfile, and run qproc with appropriate options

    Args:
        rawfile: input desi-EXPID.fits.fz raw data file
        outdir: directory to write qproc-CAM-EXPID.fits files

    Options:
        ncpu: number of CPU cores to use for parallelism; serial if ncpu<=1
        cameras: list of cameras to process; default all found in rawfile

    Returns header of HDU 0 of the input raw data file, plus dictionary of return codes for each qproc process run.
    '''
    log = desiutil.log.get_logger()
    if not os.path.isdir(outdir):
        log.info('Creating {}'.format(outdir))
        os.makedirs(outdir, exist_ok=True)

    hdr = fitsio.read_header(rawfile, 0)
    if ('OBSTYPE' not in hdr) and ('FLAVOR' not in hdr):
        log.warning(
            "no obstype nor flavor keyword in first hdu header, moving to the next one"
        )
        try:
            hdr = fitsio.read_header(rawfile, 1)
        except OSError as err:
            log.error("fitsio error reading HDU 1, trying 2 then giving up")
            hdr = fitsio.read_header(rawfile, 2)
    try:
        if 'OBSTYPE' in hdr:
            obstype = hdr['OBSTYPE'].rstrip().upper()
        else:
            log.warning('Use FLAVOR instead of missing OBSTYPE')
            obstype = hdr['FLAVOR'].rstrip().upper()
        night, expid = get_night_expid_header(hdr)
    except KeyError as e:
        log.error(str(e))
        raise (e)

    #- copy coordfile to new folder for pos accuracy
    indir = os.path.abspath(os.path.dirname(rawfile))
    coord_infile = '{}/coordinates-{:08d}.fits'.format(indir, expid)
    coord_outfile = '{}/coordinates-{:08d}.fits'.format(outdir, expid)
    print(coord_infile)
    if os.path.isfile(coord_infile):
        print('copying coordfile')
        copyfile(coord_infile, coord_outfile)
    else:
        log.warning('No coordinate file for positioner accuracy')

    #- HACK: Workaround for data on 20190626/27 that have blank NIGHT keywords
    #- Note: get_night_expid_header(hdr) should take care of this now, but
    #-       this is left in for robustness just in case
    if night == '        ' or night is None:
        log.error(
            'Correcting blank NIGHT keyword based upon directory structure')
        #- /path/to/NIGHT/EXPID/rawfile.fits
        night = os.path.basename(
            os.path.dirname(os.path.dirname(os.path.abspath(rawfile))))
        if re.match('20\d{6}', night):
            log.info('Setting NIGHT to {}'.format(night))
        else:
            raise RuntimeError('Unable to derive NIGHT for {}'.format(rawfile))

    cmdlist = list()
    loglist = list()
    msglist = list()
    rawcameras = which_cameras(rawfile)
    if cameras is None:
        cameras = rawcameras
    elif len(set(cameras) - set(rawcameras)) > 0:
        missing_cameras = set(cameras) - set(rawcameras)
        for cam in sorted(missing_cameras):
            log.error('{} missing camera {}'.format(os.path.basename(rawfile),
                                                    cam))
        cameras = sorted(set(cameras) & set(rawcameras))

    for camera in cameras:
        outfiles = dict(
            rawfile=rawfile,
            fibermap='{}/fibermap-{:08d}.fits'.format(outdir, expid),
            logfile='{}/qproc-{}-{:08d}.log'.format(outdir, camera, expid),
            outdir=outdir,
            camera=camera)

        cmd = "desi_qproc -i {rawfile} --fibermap {fibermap} --auto --auto-output-dir {outdir} --cam {camera}".format(
            **outfiles)
        cmdlist.append(cmd)
        loglist.append(outfiles['logfile'])
        msglist.append('qproc {}/{} {}'.format(night, expid, camera))

    ncpu = min(len(cmdlist), get_ncpu(ncpu))

    if ncpu > 1 and len(cameras) > 1:
        log.info('Running qproc in parallel on {} cores for {} cameras'.format(
            ncpu, len(cameras)))
        pool = mp.Pool(ncpu)
        errs = pool.starmap(runcmd, zip(cmdlist, loglist, msglist))
        pool.close()
        pool.join()
    else:
        errs = []
        log.info('Running qproc serially for {} cameras'.format(len(cameras)))
        for cmd, logfile, msg in zip(cmdlist, loglist, msglist):
            err = runcmd(cmd, logfile, msg)
            errs.append(err)

    errorcodes = dict()
    for err in errs:
        for key in err.keys():
            errorcodes[key] = err[key]

    jsonfile = '{}/errorcodes-{:08d}.txt'.format(outdir, expid)
    with open(jsonfile, 'w') as outfile:
        json.dump(errorcodes, outfile)
        print('Wrote {}'.format(jsonfile))

    return hdr
Example #6
0
def main(args):
    """Command-line driver for updating the survey plan.
    """
    # Check for a valid fa-delay value.
    if args.fa_delay[-1] not in ('d', 'm', 'q'):
        raise ValueError('fa-delay must have the form Nd, Nm or Nq.')
    fa_delay_type = args.fa_delay[-1]
    try:
        fa_delay = int(args.fa_delay[:-1])
    except ValueError:
        raise ValueError('invalid number in fa-delay.')
    if fa_delay < 0:
        raise ValueError('fa-delay value must be >= 0.')

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

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

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

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

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

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

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

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

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

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

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

    bookmarked = False
    if not args.create:

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

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

        # Will update design HA assignments here...
        pass

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

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

    # Save the plan.
    plan.write(config.get_path('plan.fits'), overwrite=True)
    if bookmarked:
        # Save a backup of the plan and progress at this point.
        plan.write(config.get_path('plan_{0}.fits'.format(start)))
        progress.save('progress_{0}.fits'.format(start))
Example #7
0
    def run(self, indir, outfile=None, jsonfile=None):
        '''TODO: document'''
        log = desiutil.log.get_logger()
        log.debug('Running QA in {}'.format(indir))
        print('here qa.runner', indir)
        preprocfiles = sorted(glob.glob('{}/preproc-*.fits'.format(indir)))
        if len(preprocfiles) == 0:
            log.error('No preproc files found in {}'.format(indir))
            return None

        # We can have different obstypes (signal+dark) with calibration data
        # obtained with a calibration slit hooked to a single spectrograph.
        # So we have to loop over all frames to check if there are science,
        # arc, or flat obstypes as guessed by qproc.
        qframefiles = sorted(glob.glob('{}/qframe-*.fits'.format(indir)))
        if len(qframefiles) == 0:  # no qframe so it's either zero or dark
            hdr = fitsio.read_header(preprocfiles[0], 0)
            if 'OBSTYPE' in hdr:
                obstype = hdr['FLAVOR'].strip()
            else:
                log.warning("Using FLAVOR instead of missing OBSTYPE")
                obstype = hdr['FLAVOR'].strip()
        else:
            obstype = None
            log.debug("Reading qframe headers to guess flavor ...")
            for qframefile in qframefiles:  # look at all of them and prefer arc or flat over dark or zero
                hdr = fitsio.read_header(qframefile, 0)
                if 'OBSTYPE' in hdr:
                    this_obstype = hdr['OBSTYPE'].strip().upper()
                else:
                    log.warning("Using FLAVOR instead of missing OBSTYPE")
                    obstype = hdr['FLAVOR'].strip()

                if this_obstype == "ARC" or this_obstype == "FLAT" \
                   or this_obstype  == "TESTARC" or this_obstype == "TESTFLAT" :
                    obstype = this_obstype
                    # we use this so we exit the loop
                    break
                elif obstype == None:
                    obstype = this_obstype
                    # we stay in the loop in case another frame has another obstype

        log.debug('Found OBSTYPE={} files'.format(obstype))

        results = dict()

        for qa in self.qalist:
            if qa.valid_obstype(obstype):
                log.info('{} Running {} {}'.format(timestamp(), qa,
                                                   qa.output_type))
                qa_results = None
                try:
                    qa_results = qa.run(indir)
                except Exception as err:
                    log.warning('{} failed on {} because {}; skipping'.format(
                        qa, indir, str(err)))
                    exc_info = sys.exc_info()
                    traceback.print_exception(*exc_info)
                    del exc_info
                    #raise(err)
                    #- TODO: print traceback somewhere useful

                if qa_results is not None:
                    if qa.output_type not in results:
                        results[qa.output_type] = list()
                    results[qa.output_type].append(qa_results)

            else:
                log.debug('Skip {} {} for {}'.format(qa, qa.output_type,
                                                     obstype))
        #- Combine results for different types of QA
        join_keys = dict(
            PER_AMP=['NIGHT', 'EXPID', 'SPECTRO', 'CAM', 'AMP'],
            PER_CAMERA=['NIGHT', 'EXPID', 'SPECTRO', 'CAM'],
            PER_FIBER=['NIGHT', 'EXPID', 'SPECTRO', 'FIBER'],
            PER_CAMFIBER=['NIGHT', 'EXPID', 'SPECTRO', 'CAM', 'FIBER'],
            PER_SPECTRO=['NIGHT', 'EXPID', 'SPECTRO'],
            PER_EXP=['NIGHT', 'EXPID'],
            QPROC_STATUS=['NIGHT', 'EXPID', 'SPECTRO', 'CAM'])

        if jsonfile is not None:
            if os.path.exists(jsonfile):
                with open(jsonfile, 'r') as myfile:
                    json_data = myfile.read()
                json_data = json.loads(json_data)
            else:
                json_data = dict()

            rewrite_necessary = False
            for key1 in results:
                if key1 != "PER_CAMFIBER" and key1.startswith("PER_"):
                    colnames_lst = results[key1][0].colnames
                    if key1 in join_keys:
                        for i in join_keys[key1]:
                            colnames_lst.remove(i)

                    if key1 not in json_data:
                        json_data[key1] = colnames_lst
                        rewrite_necessary = True
                    else:
                        for aspect in colnames_lst:
                            if aspect not in json_data[key1]:
                                json_data[key1] += [aspect]
                                rewrite_necessary = True

            if rewrite_necessary:
                if os.path.isdir(os.path.dirname(jsonfile)):
                    with open(jsonfile, 'w') as out:
                        json.dump(json_data, out)
                    print('Wrote {}'.format(jsonfile))

        for qatype in list(results.keys()):
            if len(results[qatype]) == 1:
                results[qatype] = results[qatype][0]
            else:
                tx = results[qatype][0]
                for i in range(1, len(results[qatype])):
                    tx = join(tx,
                              results[qatype][i],
                              keys=join_keys[qatype],
                              join_type='outer')
                results[qatype] = tx

            #- convert python string to bytes for FITS format compatibility
            if 'AMP' in results[qatype].colnames:
                results[qatype]['AMP'] = results[qatype]['AMP'].astype('S1')
            if 'CAM' in results[qatype].colnames:
                results[qatype]['CAM'] = results[qatype]['CAM'].astype('S1')

            #- TODO: NIGHT/EXPID/SPECTRO/FIBER int64 -> int32 or int16
            #- TODO: metrics from float64 -> float32

        if outfile is not None:
            for tx in results.values():
                night = tx['NIGHT'][0]
                expid = tx['EXPID'][0]
                break

            #- To do: consider propagating header from indir/desi*.fits.fz
            log.info('{} Writing {}'.format(timestamp(), outfile))
            tmpfile = outfile + '.tmp'
            with fitsio.FITS(tmpfile, 'rw', clobber=True) as fx:
                fx.write(np.zeros(3, dtype=float),
                         extname='PRIMARY',
                         header=hdr)
                for qatype, qatable in results.items():
                    fx.write_table(qatable.as_array(),
                                   extname=qatype,
                                   header=hdr)

            os.rename(tmpfile, outfile)
            log.info('{} Finished writing {}'.format(timestamp(), outfile))

        return results
Example #8
0
    def run(self, indir):
        '''TODO: document'''

        log = desiutil.log.get_logger()

        results = list()

        infiles = glob.glob(os.path.join(indir, 'qcframe-*.fits'))
        if len(infiles) == 0:
            log.error("no qcframe in {}".format(indir))
            return None

        # find number of spectros
        spectros = []
        for filename in infiles:
            hdr = fitsio.read_header(filename)
            s = int(hdr['CAMERA'][1])
            spectros.append(s)
        spectros = np.unique(spectros)

        for spectro in spectros:

            infiles = glob.glob(
                os.path.join(indir, 'qcframe-?{}-*.fits'.format(spectro)))

            qframes = {}
            fmap = None
            for infile in infiles:
                qframe = read_qframe(infile)
                cam = qframe.meta["CAMERA"][0].upper()
                qframes[cam] = qframe

                if fmap is None:
                    fmap = qframe.fibermap
                    night = int(qframe.meta['NIGHT'])
                    expid = int(qframe.meta['EXPID'])

                    # need to decode fibermap
                    columns, masks, survey = targets.main_cmx_or_sv(fmap)
                    desi_target = fmap[columns[0]]  # (SV1_)DESI_TARGET
                    desi_mask = masks[0]  # (sv1) desi_mask
                    stars = (
                        desi_target
                        & desi_mask.mask('STD_WD|STD_FAINT|STD_BRIGHT')) != 0
                    qsos = (desi_target & desi_mask.QSO) != 0


#             sw  = np.zeros(self.rwave.size)
#             swf = np.zeros(self.rwave.size)

#- we need a ridiculously large array to cover the r filter, but precache
#- what wavelength ranges matter for each band
            iiband = dict()
            for c in ["B", "R", "Z"]:
                if c in qframes:
                    qframe = qframes[c]
                    iiband[c] = (qframe.wave[0][0] < self.rwave)
                    iiband[c] &= (self.rwave < qframe.wave[0][-1])

            #- for each fiber, generate list of arguments to pass to get_dico
            #- get_fiber_data extracts data for only *one* fiber from qframes, fmap, which have data for all fibers
            #- this reduces the parallel processing overhead
            argslist = [(self, iiband,
                         get_fiber_data(qframes, fmap, f, fiber, night, expid,
                                        spectro, stars, qsos))
                        for f, fiber in enumerate(fmap["FIBER"])]

            ncpu = get_ncpu(None)

            if ncpu > 1:
                pool = mp.Pool(ncpu)
                results = pool.starmap(get_dico, argslist)
            else:
                for args in argslist:
                    results.append(get_dico(**args))

        if len(results) == 0:
            return None
        return Table(results, names=results[0].keys())