示例#1
0
def raw(rawfile,
        channel=120,
        transitspeed=3,
        calfile=None,
        soundspeed=None,
        absorption=None,
        preraw=None):
    """
    Read EK60 raw data.
    """

    # -------------------------------------------------------------------------
    # load rawfile
    ek60 = EK60.EK60()
    ek60.read_raw(rawfile)

    # -------------------------------------------------------------------------
    # read frequency channel data
    logger.info('Reading File ' + rawfile.split('/')[-1] + '...')
    ch = None
    for i in ek60.channel_id_map:
        if str(channel) + ' kHz' in ek60.channel_id_map[i]:
            ch = i
            break
    if ch is None:
        sys.exit(str(channel) + ' kHz channel not found!')
    raw = ek60.get_raw_data(channel_number=ch)

    # -------------------------------------------------------------------------
    # apply 38 kHz calibration parameters
    if calfile is not None:
        params = readCAL.ices(calfile, channel)

    # -------------------------------------------------------------------------
    # correct data for speed of sound and absorption
    if 'params' not in locals():

        class params(object):
            pass

    if soundspeed is not None:
        params.sound_velocity = soundspeed
    if absorption is not None:
        params.absorption_coefficient = absorption

    # -------------------------------------------------------------------------
    # get raw data
    Sv = np.transpose(raw.get_Sv(calibration=params).data)
    theta = np.transpose(raw.angles_alongship_e)
    phi = np.transpose(raw.angles_athwartship_e)
    t = raw.get_Sv(calibration=params).ping_time
    r = raw.get_Sv(calibration=params).range
    alpha = raw.absorption_coefficient[0]

    # -------------------------------------------------------------------------
    # check continuity with preceeding RAW file
    if preraw is None:
        logger.warn('no preceding RAW file')
        continuous = False
    else:
        pingrate = np.float64(np.mean(np.diff(t)))
        timelapse = np.float64(t[0] - preraw['t'][-1])
        if timelapse <= 0:
            logger.warn('no preceding RAW file')
            continuous = False
        else:
            if timelapse > 1.5 * pingrate:
                logger.warn('time breach in preceding RAW file')
                continuous = False
            else:
                if preraw['r'].size != r.size:
                    logger.warn('range discrepancy in preceding RAW file')
                    continuous = False
                else:
                    if (preraw['r'] != r).all():
                        logger.warn('range discrepancy in preceding RAW file')
                        continuous = False
                    else:
                        continuous = True

    # -------------------------------------------------------------------------
    # get nmea data
    transect, Tpos, LON, LAT, lon, lat, nm, km, kph, knt = nmea(ek60,
                                                                t,
                                                                preraw=preraw)
    if preraw is not None:
        if transect != preraw['transect']:
            continuous = False

    # -------------------------------------------------------------------------
    # if not continuous, resume distances an start a new transect
    if not continuous:
        km -= np.nanmin(km)
        nm -= np.nanmin(nm)
        if preraw is not None:
            if transect == preraw['transect']:
                transect += 1

    # -------------------------------------------------------------------------
    # if stationary, turn transect number to negative & resume distances
    if (nm[-1] - nm[0]) / (np.float64(t[-1] - t[0]) /
                           (1000 * 60 * 60)) < transitspeed:
        km -= np.nanmin(km)
        nm -= np.nanmin(nm)
        if preraw is None:
            transect = 0
        else:
            if preraw['transect'] > 0:
                transect = -preraw['transect']
            else:
                transect = preraw['transect']

    # -------------------------------------------------------------------------
    # if just went off station, go to next transect number & resume distances
    else:
        if preraw is not None:
            if (transect > 0) & (preraw['transect'] <= 0):
                transect += 1
                km -= np.nanmin(km)
                nm -= np.nanmin(nm)

    Tmot, PITCH, ROLL, HEAVE, pitch, roll, heave, pitchmax, rollmax, heavemax = motion(
        ek60, t, preraw=preraw)

    # -------------------------------------------------------------------------
    # delete objects to free up memory RAM
    del ek60, raw

    # -------------------------------------------------------------------------
    # return RAW data
    raw = {
        'rawfiles': [os.path.split(rawfile)[-1]],
        'continuous': continuous,
        'transect': transect,
        'Sv': Sv,
        'theta': theta,
        'phi': phi,
        'alpha': alpha,
        'r': r,
        't': t,
        'lon': lon,
        'lat': lat,
        'nm': nm,
        'km': km,
        'kph': kph,
        'knt': knt,
        'pitch': pitch,
        'roll': roll,
        'heave': heave,
        'pitchmax': pitchmax,
        'rollmax': rollmax,
        'heavemax': heavemax,
        'Tpos': Tpos,
        'LON': LON,
        'LAT': LAT,
        'Tmot': Tmot,
        'PITCH': PITCH,
        'ROLL': ROLL,
        'HEAVE': HEAVE
    }

    return raw
示例#2
0
example is to verify basic operation of the insert and delete methods,
but it also provides some simple and somewhat contrived examples of using
index arrays with these methods.

Note that the Echogram class doesn't always handle ping times that are NaT
very well so the X axis doesn't always get labeled properly in this example.
"""

import numpy as np
from matplotlib.pyplot import figure, show, subplots_adjust
from echolab2.instruments import EK60
from echolab2.plotting.matplotlib import echogram

# Read in some data from files.
rawfiles = ['./data/EK60/DY1201_EK60-D20120214-T231011.raw']
ek60 = EK60.EK60()
ek60.read_raw(rawfiles)

# Get a reference to the raw_data object.
raw_data_38 = ek60.get_raw_data(channel_number=2)
print(raw_data_38)

# Insert synthetic data.  Create data where each ping is a constant value and
# the values change significantly from ping to ping so it is easy to
# distinguish the pings and verify (or not) that data is in the right place.
fake_data = np.arange(20) * 9.0 - 150.0
j = 0
for i in range(raw_data_38.n_pings):
    raw_data_38.power[i, :] = fake_data[j]
    j += 1
    if j == fake_data.shape[0]:
示例#3
0
def write(rawdir, save=False, txtfile='filereport.txt',
           maxspeed=20, precision=8, bathymetry=None):
    """
    Write a report with NMEA and CONFIG information for each RAW file contained 
    in a directory.
    
    Args:
        rawdir   (str ): Directory were the RAW files are stored.
        save     (bool): Whether or not to save data as a text file in rawdir.
        txtfile  (str ): Name of text file created.
        maxspeed (int ): Max speed permited in knots. Above this, it will
                         be considered an error and set to NAN.
        precision (int): Decimal precision of data printed.
        bathyrmetry (str): path/to/bathymetry/GEBCO.nc (default: None)
        
    Returns:
        Pandas.DataFrame: Report with the following tabular data:
            * file  : Name of RAW file
            * y0    : start year (years after AD epoch)
            * y1    : end year (years after AD epoch)
            * d0    : start day (day of year)
            * d1    : end day (day of year)
            * h0    : start time (decimal hours)
            * h1    : end time (decimal hours)
            * sun0  : sun altitude at the start (degreses)
            * sun1  : sun altitude at the end (degrees)
            * seabed: seabed depth, estimated from bathymetry (m)
            * lonmin: longitude minimum (decimal degrees)
            * lonp25: longitude percentile 25 (decimal degrees)
            * lonmed: longitude median (decimal degrees)
            * lonp75: longitude percentile 75 (decimal degrees)
            * lonmax: longitude maximum (decimal degrees)
            * latmin: latitude minimum (decimal degrees)
            * latp25: latitude percentile 25 (decimal degrees)
            * latmed: latitude median (decimal degrees)
            * latp75: latitude percentile 75 (decimal degrees)
            * latmax: latitude maximum (decimal degrees)
            * kntmin: speed minimum (knots)
            * kntp25: speed percentile 25 (knots)
            * kntmed: speed median (knots)
            * kntp75: speed percentile 75 (knots)
            * kntmax: speed maximum (knots)
            * f18   : Presence of 18 kHz frequency (boolean)
            * p18   : Power at 18 kHz (w)
            * l18   : Pulse length at 18 kHz (m)
            * i18   : Sample interval at 18 kHz (m)
            * c18   : Sample count at 18 kHz)
            * f38   : Presence of 38 kHz frequency (boolean)
            * p38   : Power at 38 kHz (w)
            * l38   : Pulse length at 38 kHz (m)
            * i38   : Sample interval at 38 kHz (m)
            * c38   : Sample count at 38 kHz
            * f70   : Presence of 70 kHz frequency (boolean)
            * p70   : Power at 70 kHz (w)
            * l70   : Pulse length at 70 kHz (m)
            * i70   : Sample interval at 70 kHz (m)
            * c70   : Sample count at 70 kHz
            * f120  : Presence of 120 kHz frequency (boolean)
            * p120  : Power at 120 kHz (w)
            * l120  : Pulse length at 120 kHz (m)
            * i120  : Sample interval at 120 kHz (m)
            * c120  : Sample count at 120 kHz
            * f200  : Presence of 200 kHz frequency (boolean)
            * p200  : Power at 200 kHz (w)
            * l200  : Pulse length at 200 kHz (m)
            * i200  : Sample interval at 200 kHz (m)
            * c200  : Sample count at 200 kHz
            * f333  : Presence of 333 kHz frequency (boolean)
            * p333  : Power at 333 kHz (w)
            * l333  : Pulse length at 333 kHz (m)
            * i333  : Sample interval at 333 kHz (m)
            * c333  : Sample count at 333 kHz         
    """
    
    # load bathymetry file and preallocate bathymetry data
    if bathymetry:
        bfile = xarray.open_dataset(bathymetry, chunks={'lon':360,'lat':180})                  
        bdata = None
        
    # look for RAW files in the directory
    rawfiles = np.sort(glob.glob(os.path.join(rawdir, '*.raw')))
    if not len(rawfiles):
        raise Exception('No RAW files in directory %s' % rawdir)
    
    # create data frame and declare columns names
    pd.set_option('precision', precision)
    df = pd.DataFrame(columns=[
                      'file'  ,
                      'y0'    ,'y1'    ,
                      'd0'    ,'d1'    , 
                      'h0'    ,'h1'    ,
                      'sun0'  ,'sun1'  ,
                      'seabed',                                 
                      'lonmin','lonp25','lonmed','lonp75','lonmax',
                      'latmin','latp25','latmed','latp75','latmax',
                      'kntmin','kntp25','kntmed','kntp75','kntmax',                               
                      'f18'   ,'f38'   ,'f70'   ,'f120'  ,'f200'  ,'f333',
                      'p18'   ,'p38'   ,'p70'   ,'p120'  ,'p200'  ,'p333',
                      'l18'   ,'l38'   ,'l70'   ,'l120'  ,'l200'  ,'l333',
                      'i18'   ,'i38'   ,'i70'   ,'i120'  ,'i200'  ,'i333',
                      'c18'   ,'c38'   ,'c70'   ,'c120'  ,'c200'  ,'c333'])
    
    # iterate through RAW files
    errors = []
    for i, rawfile in enumerate(rawfiles):
        print('%s of %s: Collecting summary data from file %s' %
             (i+1, len(rawfiles), os.path.split(rawfile)[-1]))
        
        # load RAW file
        try:
            ek60 = EK60.EK60()
            ek60.read_raw(rawfile)
        
        # if error, add empty row to the data frame, log error, and continue
        except Exception:
             print('Unable to read file %s' % 
                   os.path.split(rawfile)[-1])
             df.loc[i] = [os.path.split(rawfile)[-1]]+['']*(df.shape[1]-1)
             errors.append(os.path.split(rawfile)[-1])
             continue         
        
        # get year, day of year, and decimal hour from  start datetime
        dt0 = ek60.start_time
        dt0 = pd.DatetimeIndex([dt0])
        y0  = np.int64(dt0.year)[0]
        d0  = np.int64(dt0.dayofyear)[0]
        h0  = np.float64(dt0.hour
                         + dt0.minute     /60 
                         + dt0.second     /3600
                         + dt0.microsecond/3600e6)[0]
        
        # get year, day of year, and decimal hour from end datetime
        dt1 = ek60.end_time
        dt1 = pd.DatetimeIndex([dt1])
        y1  = np.int64(dt1.year)[0]
        d1  = np.int64(dt1.dayofyear)[0]
        h1  = np.float64(dt1.hour
                         + dt1.minute     /60 
                         + dt1.second     /3600
                         + dt1.microsecond/3600e6)[0]
        
        # explore NMEA messages to get position data, break when got it
        for msg in ['GGA', 'GLL', 'RMC']:
            time= ek60.nmea_data.get_datagrams(msg,return_fields=['Time'])
            time= time[msg]['time']    
            lon = ek60.nmea_data.get_datagrams(msg,return_fields=['longitude'])
            lon = lon[msg]['longitude']
            lat = ek60.nmea_data.get_datagrams(msg,return_fields=['latitude'])
            lat = lat[msg]['latitude' ]
            if (isinstance(time, np.ndarray)&
                isinstance(lon , np.ndarray)&
                isinstance(lat , np.ndarray)):
                break
        
        # set metrics to NAN if NMEA data was not found   
        if (time is None)|(lon is None)|(lat is None):
            nan= np.nan
            sun0  , sun1                           = nan, nan
            seabed                                 = nan
            lonmin, lonp25, lonmed, lonp75, lonmax = nan, nan, nan, nan, nan
            latmin, latp25, latmed, latp75, latmax = nan, nan, nan, nan, nan
            kntmin, kntp25, kntmed, kntp75, kntmax = nan, nan, nan, nan, nan
            warnings.warn('NMEA position data not found', Warning)
        
        # calculate metrics otherwise
        else:
        
            # allocate longitude metrics    
            lonmin = np.min(lon)
            lonp25 = np.percentile(lon, 25)
            lonmed = np.median(lon)
            lonp75 = np.percentile(lon, 75)
            lonmax = np.max(lon)
            
            # allocate latitude metrics    
            latmin = np.min(lat)
            latp25 = np.percentile(lat, 25)
            latmed = np.median(lat)
            latp75 = np.percentile(lat, 75)
            latmax = np.max(lat)
            
            # calculate speed in knots
            nmi = np.zeros(len(lon))*np.nan
            for j in range(len(lon)-1):
                if np.isnan(np.array([lon[j],lon[j+1],lat[j],lat[j+1]])).any():
                    nmi[j+1] = np.nan
                else:
                    nmi[j+1] = distance((lat[j],lon[j]),(lat[j+1],lon[j+1])).nm
            knt = np.r_[np.nan, nmi[1:]/(np.float64(np.diff(time))/1000)*3600]
            
            # remove values above max speed
            if (knt[~np.isnan(knt)]>maxspeed).any():
                warnings.warn('%s speeds out of %s above max speed: set to NAN'
                              % (sum(knt[~np.isnan(knt)]>maxspeed), len(nmi)),
                              Warning)        
                knt[np.ma.masked_greater(knt, maxspeed).mask] = np.nan
            
            # allocate speed metrics    
            kntmin = np.nanmin(knt)
            kntp25 = np.nanpercentile(knt, 25)
            kntmed = np.nanmedian(knt)
            kntp75 = np.nanpercentile(knt, 75)
            kntmax = np.nanmax(knt)
            
            # get sun altitude in degrees with respect to platform positions
            # at the first and last timestamp.
            p0      = ephem.Observer()
            p0.lon  = str(lon[0])    
            p0.lat  = str(lat[0])   
            p0.date = pd.to_datetime(time[0]).strftime('%Y/%m/%d %H:%M:%S')
            sun0str = str(ephem.Sun(p0).alt)
            deg0    = float(sun0str.split(':')[0])
            min0    = float(sun0str.split(':')[1])
            sec0    = float(sun0str.split(':')[2])
            sun0    = deg0 + min0/60 + sec0/3600
            p1      = ephem.Observer()
            p1.lon  = str(lon[-1])    
            p1.lat  = str(lat[-1])   
            p1.date = pd.to_datetime(time[-1]).strftime('%Y/%m/%d %H:%M:%S')
            sun1str = str(ephem.Sun(p1).alt)
            deg1    = float(sun1str.split(':')[0])
            min1    = float(sun1str.split(':')[1])
            sec1    = float(sun1str.split(':')[2])
            sun1 = deg1 + min1/60 + sec1/3600
            
            # estimate seabed depth
            if bathymetry:
                
                # load bathymetry data if not done yet
                if bdata is None:
                    c = (lonmed-1,lonmed+1,latmed+1,latmed-1) # W, E, N, S
                    print('Loading bathymetry [%2.2fW, %2.2fE, %2.2fN, %2.2fS]...'%c)
                    bdata = bfile.where((c[0]<bfile.lon)&(bfile.lon<c[1])&
                                        (c[3]<bfile.lat)&(bfile.lat<c[2]),
                                        drop=True)
                    blon = bdata.lon.data
                    blat = bdata.lat.data
                    bele = bdata.elevation.data.compute()
                
                # use bathymetry to estimate seabed at RAW file's position                    
                f = interp2d(blon, blat, bele, bounds_error=True)                        
                try:
                    seabed = int(f(lonmed,latmed)[0])
                
                # reload  bathymetry if file's position falls outside the area
                except ValueError:
                    c = (lonmed-1,lonmed+1,latmed+1,latmed-1) # W, E, N, S
                    print('Reloading bathymetry [%sW, %sE, %sN, %sS]...'%c)
                    bdata = bfile.where((c[0]<bfile.lon)&(bfile.lon<c[1])&
                                        (c[3]<bfile.lat)&(bfile.lat<c[2]),
                                        drop=True)
                    blon = bdata.lon.data
                    blat = bdata.lat.data
                    bele = bdata.elevation.data.compute()
                    f = interp2d(blon, blat, bele, bounds_error=True)
                    seabed = int(f(lonmed,latmed)[0])
            
            # fill seabed depth with NAN, if bathymetry is not provided
            else:
                seabed=np.nan
        
        # collect configuration for each frequency
        f18,f38,f70,f120,f200,f333 =  0, 0, 0, 0, 0, 0
        p18,p38,p70,p120,p200,p333 = np.nan,np.nan,np.nan,np.nan,np.nan,np.nan
        l18,l38,l70,l120,l200,l333 = np.nan,np.nan,np.nan,np.nan,np.nan,np.nan
        i18,i38,i70,i120,i200,i333 = np.nan,np.nan,np.nan,np.nan,np.nan,np.nan
        c18,c38,c70,c120,c200,c333 = np.nan,np.nan,np.nan,np.nan,np.nan,np.nan        
        
        for ch, freq in enumerate(ek60.channel_ids):    
            
            # at 18 kHz
            if '18 kHz' in freq:
                f18 = ek60.get_raw_data(channel_number=ch+1)
                if len(np.unique(f18.transmit_power))==1:
                    p18 = f18.transmit_power[0]
                else:
                    p18 = -1
                if len(np.unique(f18.pulse_length))==1:
                    l18 = f18.pulse_length[0]
                else:
                    l18 = -1
                if len(np.unique(f18.sample_interval))==1:
                    i18 = f18.sample_interval[0]
                else:
                    i18 = -1
                if len(np.unique(f18.sample_count))==1:
                    c18 = f18.sample_count[0]
                else:
                    c18 = -1
                f18 = 1
            
            # at 38 kHz
            if '38 kHz' in freq:
                f38 = ek60.get_raw_data(channel_number=ch+1)
                if len(np.unique(f38.transmit_power))==1:
                    p38 = f38.transmit_power[0]
                else:
                    p38 = -1
                if len(np.unique(f38.pulse_length))==1:
                    l38 = f38.pulse_length[0]
                else:
                    l38 = -1
                if len(np.unique(f38.sample_interval))==1:
                    i38 = f38.sample_interval[0]
                else:
                    i38 = -1
                if len(np.unique(f38.sample_count))==1:
                    c38 = f38.sample_count[0]
                else:
                    c38 = -1
                f38 = 1
            
            # at 70 kHz
            if '70 kHz' in freq:
                f70 = ek60.get_raw_data(channel_number=ch+1)
                if len(np.unique(f70.transmit_power))==1:
                    p70 = f70.transmit_power[0]
                else:
                    p70 = -1
                if len(np.unique(f70.pulse_length))==1:
                    l70 = f70.pulse_length[0]
                else:
                    l70 = -1
                if len(np.unique(f70.sample_interval))==1:
                    i70 = f70.sample_interval[0]
                else:
                    i70 = -1
                if len(np.unique(f70.sample_count))==1:
                    c70 = f70.sample_count[0]
                else:
                    c70 = -1
                f70 = 1
            
            # at 120 kHz
            if '120 kHz' in freq:
                f120 = ek60.get_raw_data(channel_number=ch+1)
                if len(np.unique(f120.transmit_power))==1:
                    p120 = f120.transmit_power[0]
                else:
                    p120 = -1
                if len(np.unique(f120.pulse_length))==1:
                    l120 = f120.pulse_length[0]
                else:
                    l120 = -1
                if len(np.unique(f120.sample_interval))==1:
                    i120 = f120.sample_interval[0]
                else:
                    i120 = -1
                if len(np.unique(f120.sample_count))==1:
                    c120 = f120.sample_count[0]
                else:
                    c120 = -1
                f120 = 1
            
            # at 200 kHz
            if '200 kHz' in freq:
                f200 = ek60.get_raw_data(channel_number=ch+1)
                if len(np.unique(f200.transmit_power))==1:
                    p200 = f200.transmit_power[0]
                else:
                    p200 = -1
                if len(np.unique(f200.pulse_length))==1:
                    l200 = f200.pulse_length[0]
                else:
                    l200 = -1
                if len(np.unique(f200.sample_interval))==1:
                    i200 = f200.sample_interval[0]
                else:
                    i200 = -1
                if len(np.unique(f200.sample_count))==1:
                    c200 = f200.sample_count[0]
                else:
                    c200 = -1
                f200 = 1
            
            # at 333 kHz
            if '333 kHz' in freq:
                f333 = ek60.get_raw_data(channel_number=ch+1)
                if len(np.unique(f333.transmit_power))==1:
                    p333 = f333.transmit_power[0]
                else:
                    p333 = -1
                if len(np.unique(f333.pulse_length))==1:
                    l333 = f333.pulse_length[0]
                else:
                    l333 = -1
                if len(np.unique(f333.sample_interval))==1:
                    i333 = f333.sample_interval[0]
                else:
                    i333 = -1
                if len(np.unique(f333.sample_count))==1:
                    c333 = f333.sample_count[0]
                else:
                    c333 = -1
                f333 = 1
        
        # add new row of data to dataframe 
        df.loc[i] = [os.path.split(rawfile)[-1],
                     y0    , y1    ,
                     d0    , d1    , 
                     h0    , h1    ,
                     sun0  , sun1  ,
                     seabed,
                     lonmin, lonp25, lonmed, lonp75, lonmax,
                     latmin, latp25, latmed, latp75, latmax,
                     kntmin, kntp25, kntmed, kntp75, kntmax,
                     f18   , f38   , f70   , f120  , f200  , f333,
                     p18   , p38   , p70   , p120  , p200  , p333,
                     l18   , l38   , l70   , l120  , l200  , l333,
                     i18   , i38   , i70   , i120  , i200  , i333,
                     c18   , c38   , c70   , c120  , c200  , c333]
    
    # save data frame as a text file
    if save:
        with open(os.path.join(rawdir, txtfile), 'w+') as f:
            
            # write header
            f.write('# NMEA AND CONFIGURATION DATA SUMMARY:\n')
            f.write('# Variable names per column, from left to right:\n')
            f.write('# * file  : Name of RAW file\n')
            f.write('# * y0    : start year (years after AD epoch)\n')
            f.write('# * y1    : end year (years after AD epoch)\n')
            f.write('# * d0    : start day (day of year)\n')
            f.write('# * d1    : end day (day of year)\n')
            f.write('# * h0    : start time (decimal hours)\n')
            f.write('# * h1    : end time (decimal hours)\n')
            f.write('# * sun0  : sun altitude at the start (degreses)\n')
            f.write('# * sun1  : sun altitude at the end (degrees)\n')
            f.write('# * seabed: seabed depth, estimated from bathymetry (m)\n')
            f.write('# * lonmin: longitude minimum (decimal degrees)\n')
            f.write('# * lonp25: longitude percentile 25 (decimal degrees)\n')
            f.write('# * lonmed: longitude median (decimal degrees)\n')
            f.write('# * lonp75: longitude percentile 75 (decimal degrees)\n')
            f.write('# * lonmax: longitude maximum (decimal degrees)\n')
            f.write('# * latmin: latitude minimum (decimal degrees)\n')
            f.write('# * latp25: latitude percentile 25 (decimal degrees)\n')
            f.write('# * latmed: latitude median (decimal degrees)\n')
            f.write('# * latp75: latitude percentile 75 (decimal degrees)\n')
            f.write('# * latmax: latitude maximum (decimal degrees)\n')
            f.write('# * kntmin: speed minimum (knots)\n')
            f.write('# * kntp25: speed percentile 25 (knots)\n')
            f.write('# * kntmed: speed median (knots)\n')
            f.write('# * kntp75: speed percentile 75 (knots)\n')
            f.write('# * kntmax: speed maximum (knots)\n')
            f.write('# * f18   : Presence of 18 kHz frequency (boolean)\n')            
            f.write('# * f38   : Presence of 38 kHz frequency (boolean)\n')            
            f.write('# * f70   : Presence of 70 kHz frequency (boolean)\n')            
            f.write('# * f120  : Presence of 120 kHz frequency (boolean)\n')            
            f.write('# * f200  : Presence of 200 kHz frequency (boolean)\n')
            f.write('# * f333  : Presence of 333 kHz frequency (boolean)\n')
            f.write('# * p18   : Power at 18 kHz (w). Fill value \'-1\' stands for \'variable\'\n')
            f.write('# * p38   : Power at 38 kHz (w). Fill value \'-1\' stands for \'variable\'\n')
            f.write('# * p70   : Power at 70 kHz (w). Fill value \'-1\' stands for \'variable\'\n')
            f.write('# * p120  : Power at 120 kHz (w). Fill value \'-1\' stands for \'variable\'\n')
            f.write('# * p200  : Power at 200 kHz (w). Fill value \'-1\' stands for \'variable\'\n')
            f.write('# * p333  : Power at 333 kHz (w). Fill value \'-1\' stands for \'variable\'\n')                    
            f.write('# * l18   : Pulse length at 18 kHz (m). Fill value \'-1\' stands for \'variable\'\n')
            f.write('# * l38   : Pulse length at 38 kHz (m). Fill value \'-1\' stands for \'variable\'\n')
            f.write('# * l70   : Pulse length at 70 kHz (m). Fill value \'-1\' stands for \'variable\'\n')
            f.write('# * l120  : Pulse length at 120 kHz (m). Fill value \'-1\' stands for \'variable\'\n')
            f.write('# * l200  : Pulse length at 200 kHz (m). Fill value \'-1\' stands for \'variable\'\n')
            f.write('# * l333  : Pulse length at 333 kHz (m). Fill value \'-1\' stands for \'variable\'\n')      
            f.write('# * i18   : Sample interval at 18 kHz (m). Fill value \'-1\' stands for \'variable\'\n')
            f.write('# * i38   : Sample interval at 38 kHz (m). Fill value \'-1\' stands for \'variable\'\n')
            f.write('# * i70   : Sample interval at 70 kHz (m). Fill value \'-1\' stands for \'variable\'\n')
            f.write('# * i120  : Sample interval at 120 kHz (m). Fill value \'-1\' stands for \'variable\'\n')
            f.write('# * i200  : Sample interval at 200 kHz (m). Fill value \'-1\' stands for \'variable\'\n')
            f.write('# * i333  : Sample interval at 333 kHz (m). Fill value \'-1\' stands for \'variable\'\n')
            f.write('# * c18   : Sample count at 18 kHz. Fill value \'-1\' stands for \'variable\'\n')
            f.write('# * c38   : Sample count at 38 kHz. Fill value \'-1\' stands for \'variable\'\n')
            f.write('# * c70   : Sample count at 70 kHz. Fill value \'-1\' stands for \'variable\'\n')
            f.write('# * c120  : Sample count at 120 kHz. Fill value \'-1\' stands for \'variable\'\n')
            f.write('# * c200  : Sample count at 200 kHz. Fill value \'-1\' stands for \'variable\'\n')
            f.write('# * c333  : Sample count at 333 kHz. Fill value \'-1\' stands for \'variable\'\n')
            f.write('\n')
            
            # write data as comma-separated values (csv)
            data = df.to_string(header=False, index=False).split('\n')
            data = [','.join(x.split()) for x in data]
            data = [x.replace('NaN','') for x in data]
            for d in data: 
                f.write(d+'\n')
    
    # report files not read due to reading errors
    if errors:
        print('Unable to read the following RAW files: %s' % errors)
        print('%s files out of %s couldn\' be read!' %
             (len(errors), len(rawfiles)))
    
    # return dataframe        
    return df