NCCOOS Trac Projects: Top | Web | Platforms | Processing | Viz | Sprints | Sandbox | (Wind)

root/raw2proc/trunk/raw2proc/raw2proc.py

Revision 491 (checked in by haines, 12 years ago)

--

  • Property svn:executable set to
Line 
1 #!/usr/bin/env python
2 # Last modified:  Time-stamp: <2012-05-01 16:40:55 haines>
3 """Process raw data to monthly netCDF data files
4
5 This module processes raw ascii- or binary-data from different NCCOOS
6 sensors (ctd, adcp, waves-adcp, met) based on manual or automated
7 operation.  If automated processing, add raw data (level0) from all
8 active sensors to current month's netcdf data files (level1) with the
9 current configuration setting.  If manual processing, determine which
10 configurations to use for requested platform, sensor, and month.
11
12 :Processing steps:
13   0. raw2proc auto or manual for platform, sensor, month
14   1. list of files to process
15   2. parse data
16   3. create, update netcdf
17
18   to-do
19   3. qc (measured) data
20   4. process derived data (and regrid?)
21   5. qc (measured and derived) data flags
22
23 """
24
25 __version__ = "v0.1"
26 __author__ = "Sara Haines <sara_haines@unc.edu>"
27
28 import sys
29 import os
30 import re
31 import traceback
32
33 # for production use:
34 # defconfigs='/home/haines/nccoos/raw2proc'
35 # for testing use:
36 # defconfigs='/home/haines/nccoos/test/r2p'
37
38 # define config file location to run under cron
39 defconfigs='/opt/env/haines/dataproc/raw2proc'
40
41 import numpy
42
43 from procutil import *
44 from ncutil import *
45
46 REAL_RE_STR = '\\s*(-?\\d(\\.\\d+|)[Ee][+\\-]\\d\\d?|-?(\\d+\\.\\d*|\\d*\\.\\d+)|-?\\d+)\\s*'
47 NAN_RE_STR = '[Nn][Aa][Nn]'
48
49 def load_data(inFile):
50     lines=None
51     if os.path.exists(inFile):
52         f = open(inFile, 'r')
53         lines = f.readlines()
54         f.close()
55         if len(lines)<=0:
56             print 'Empty file: '+ inFile           
57     else:
58         print 'File does not exist: '+ inFile
59     return lines
60
61 def import_parser(name):
62     mod = __import__('parsers')
63     parser = getattr(mod, name)
64     return parser
65
66 def import_processors(mod_name):
67     mod = __import__(mod_name)
68     parser = getattr(mod, 'parser')
69     creator = getattr(mod, 'creator')
70     updater = getattr(mod, 'updater')
71     return (parser, creator, updater)
72    
73
74 def get_config(name):
75     """Usage Example >>>sensor_info = get_config('bogue_config_20060918.sensor_info')"""
76     components = name.split('.')
77     mod = __import__(components[0])
78     for comp in components[1:]:
79         attr = getattr(mod, comp)
80     return attr
81
82 def get_config_dates(pi):
83     """ Get datetime of both start and end setting within config file
84
85     Example
86     -------
87         >>> pi = get_config(cn+'.platform_info')
88         >>> (config_start_dt, config_end_dt) = get_config_dates(pi)
89
90     """
91     now_dt = datetime.utcnow()
92     now_dt.replace(microsecond=0)
93     if pi['config_start_date']:
94         config_start_dt = filt_datetime(pi['config_start_date'])
95     elif pi['config_start_date'] == None:
96         config_start_dt = now_dt
97     if pi['config_end_date']:
98         config_end_dt = filt_datetime(pi['config_end_date'])
99     elif pi['config_end_date'] == None:
100         config_end_dt = now_dt
101     return (config_start_dt, config_end_dt)
102
103 def find_configs(platform, yyyy_mm, config_dir=''):
104     """Find which configuration files for specified platform and month
105
106     :Parameters:
107        platform : string
108            Platfrom id to process (e.g. 'bogue')
109        yyyy_mm : string
110            Year and month of data to process (e.g. '2007_07')
111
112     :Returns:
113        cns : list of str
114            List of configurations that overlap with desired month
115            If empty [], no configs were found
116     """
117     import glob
118     # list of config files based on platform
119     configs = glob.glob(os.path.join(config_dir, platform + '_config_*.py'))
120     configs.sort()
121     # determine when month starts and ends
122     (prev_month, this_month, next_month) = find_months(yyyy_mm)
123     month_start_dt = this_month
124     month_end_dt = next_month - timedelta(seconds=1)
125     # print month_start_dt; print month_end_dt
126     #
127     cns = []
128     for config in configs:
129         cn = os.path.splitext(os.path.basename(config))[0]
130         pi = get_config(cn+'.platform_info')
131         (config_start_dt, config_end_dt) = get_config_dates(pi)
132         if (config_start_dt <= month_start_dt or config_start_dt <= month_end_dt) and \
133                (config_end_dt >= month_start_dt or config_end_dt >= month_end_dt):
134             cns.append(cn)
135     return cns
136
137
138 def find_active_configs(config_dir=defconfigs):
139     """Find which configuration files are active
140
141     :Returns:
142        cns : list of str
143            List of configurations that overlap with desired month
144            If empty [], no configs were found
145     """
146     import glob
147     # list of all config files
148     configs = glob.glob(os.path.join(config_dir, '*_config_*.py'))
149     cns = []
150     for config in configs:
151         # datetime from filename
152         cn = os.path.splitext(os.path.basename(config))[0]
153         pi = get_config(cn+'.platform_info')
154         if pi['config_end_date'] == None:
155             cns.append(cn)
156     return cns
157
158
159 def uniqify(seq):
160     seen = {}
161     result = []
162     for item in seq:
163         # in old Python versions:
164         # if seen.has_key(item)
165         # but in new ones:
166         if item in seen: continue
167         seen[item] = 1
168         result.append(item)
169     return result
170                                                        
171
172 def get_all_platforms(config_dir=defconfigs):
173     """Get all platform ids
174
175     :Returns:
176        pids : list of str
177            Sorted list of all the platforms
178     """
179     import glob
180     # list of all config files
181     configs = glob.glob(os.path.join(config_dir, '*_config_*.py'))
182     configs.sort()
183     pids = []
184     for config in configs:
185         # datetime from filename
186         cn = os.path.splitext(os.path.basename(config))[0]
187         pi = get_config(cn+'.platform_info')
188         if pi['id']:
189             pids.append(pi['id'])
190     pids = uniqify(pids)
191     pids.sort()
192     return pids
193
194 def get_all_packages(platform, config_dir=defconfigs):
195     """Get all package ids -- all defined packages in sensor_info{} from all configs for the platform
196
197     :Returns:
198        sids : list of str
199            Sorted list of all the sensor ids for package
200     """
201     import glob
202     # list of all config files
203     configs = glob.glob(os.path.join(config_dir, platform + '_config_*.py'))
204     configs.sort()
205     #
206     sids = []
207     for config in configs:
208         cn = os.path.splitext(os.path.basename(config))[0]
209         pi = get_config(cn+'.platform_info')
210         sids.extend(list(pi['packages']))
211     sids = uniqify(sids)
212     sids.sort()
213     return sids
214
215 def get_all_platform_configs(platform, config_dir=defconfigs):
216     """Get all the config files for a platform
217
218     :Returns:
219        cns : list of config names
220            Sorted list of all the sensor ids for package
221     """
222     import glob
223     # list of all config files
224     configs = glob.glob(os.path.join(config_dir, platform + '_config_*.py'))
225     configs.sort()
226     #
227     cns = []
228     for config in configs:
229         cn = os.path.splitext(os.path.basename(config))[0]
230         cns.append(cn)
231     return cns
232
233 def get_config_packages(cn):
234     """ Get active packages set in platform_info{} from specific config file
235
236     :Returns:
237        sids : list of str
238            Sorted (default) or unsorted list of all the sensor ids for package
239            If empty [], no platform ids were found
240     """
241     pi = get_config(cn+'.platform_info')
242     sids = list(pi['packages'])
243     return sids
244
245 def list_months(dts, dte):
246     """ list of datetimes for all months inclusively within given date range
247    
248     """
249     lom = []
250     if type(dts) == type(dte) == type(datetime.utcnow()) and dts <= dte:
251         years = range(dts.year,dte.year+1)
252         for yyyy in years:
253             if yyyy > dts.year:
254                 a = 1
255             else:
256                 a = dts.month
257             if yyyy < dte.year:
258                 b = 12
259             else:
260                 b = dte.month
261             months = range(a, b+1)
262             for mm in months:
263                 lom.append(datetime(yyyy, mm, 1).strftime('%Y_%m'))
264     else:
265         print "list_months requires two inputs type datetime.datetime and dts<dte"
266     return lom
267    
268
269 def create_spin_list(plats, packs, dates, config_dir=defconfigs):
270     """ create list of params needed to run manual() mutiple ways
271
272     :Returns:
273        spin_list : list of three-tuple each tuple with form (platform, package, yyyy_mm)
274
275     Notes
276     -----
277
278     1. plats -- 'ALL' or ['b1', 'b2']
279     2. packs -- 'ALL' or ['ctd1', 'ctd2']
280     3. dates -- 'ALL' or ['2011_11', '2011_12'] or [dt.datetime(2006,1,1), dt.nowutc()]
281
282     For each platform determin packages for given dates
283     also a good way to get listing platforms and packages for specified dates
284
285     """
286     result = []
287     platforms = []
288     if type(plats) == str:
289         if plats.upper() == 'ALL':
290             platforms = get_all_platforms()
291         else:
292             platforms = [plats] # make one platform iterable
293     else: platforms = plats
294        
295     print ' Expanded lists for creating spin_list:'
296     print ' ...  platform ids : %s' % platforms
297
298     for platform in platforms:
299         if len(platforms)>1:
300             print '------------------------------------'
301             print ' ... ... platform : %s ' % platform
302         packages = []
303         if type(packs) == str:
304             if packs.upper() == 'ALL':
305                 packages = get_all_packages(platform)
306             else:
307                 packages = [packs] # make one package iterable
308         else: packages = packs
309
310         print ' ... ... packages : %s' % packages
311         for package in packages:
312             # dates is a string 'ALL' or format 'YYYY_MM'
313             months = []
314             if type(dates) == str:
315                 if dates.upper() == 'ALL':
316                     cns = get_all_platform_configs(platform)
317                     months = []
318                     for cn in cns:
319                         pi = get_config(cn+'.platform_info')
320                         (dts, dte) = get_config_dates(pi)
321                         if package in pi['packages']:
322                             months.extend(list_months(dts, dte))
323                 else:
324                     months = [dates] # make on date iterable
325             # dates is a list
326             if type(dates) == type([]):
327                 # if dates has two datetime types
328                 if type(dates[0]) == type(dates[1]) == type(datetime.utcnow()):
329                     dt1, dt2 = dates
330                     cns = get_all_platform_configs(platform)
331                     months = []
332                     for cn in cns:
333                         pi = get_config(cn+'.platform_info')
334                         (dts, dte) = get_config_dates(pi)
335
336                         if dts<=dt1 and dt1<=dte: a = dt1
337                         elif dt1<=dts and dt1<=dte: a = dts
338
339                         if dts<dt2 and dt2<=dte: b = dt2
340                         elif dts<dt2 and dte<=dt2: b = dte
341
342                         if dte<dt1 or dt2<dts:
343                             continue
344                         # list only months that are in configs for wide date range
345                         if package in pi['packages']:
346                             months.extend(list_months(a,b))
347                 # else if string in list
348                 elif type(dates[0]) == str:
349                     months = dates
350             print ' ... ...   months : %s' % months
351             for month in months:
352                 # print '... ... %s %s %s' % (platform, package, month)
353                 result.append((platform, package, month))
354                
355     return result
356                
357 def find_raw(si, yyyy_mm):
358     """Determine which list of raw files to process for month """
359     import glob
360
361     months = find_months(yyyy_mm)
362     # list all the raw files in prev-month, this-month, and next-month
363     all_raw_files = []
364     m = re.search('\d{4}_\d{2}$', si['raw_dir'])
365     if m:
366         # look for raw_file_glob in specific directory ending in YYYY_MM
367         # but look no further. 
368         gs = os.path.join(si['raw_dir'], si['raw_file_glob'])
369         all_raw_files.extend(glob.glob(gs))
370     else:
371         # no YYYY_MM at end of raw_dir then look for files
372         # in prev-month, this-month, and next-month
373         for mon in months:
374             mstr = mon.strftime('%Y_%m')
375             gs = os.path.join(si['raw_dir'], mstr, si['raw_file_glob'])
376             all_raw_files.extend(glob.glob(gs))
377            
378     all_raw_files.sort()
379        
380     #
381     dt_start = si['proc_start_dt']-timedelta(days=1)
382     dt_end = si['proc_end_dt']+timedelta(days=1)
383     raw_files = []; raw_dts = []
384     # compute datetime for each file
385     for fn in all_raw_files:
386         (fndt, granularity) = filt_datetime(os.path.basename(fn), gran=True)
387         if granularity == 4:
388             # change dt_start to before monthly filename filt_datetime() date
389             # for filenames with just YYYY_MM or YYYYMM add or substract 30 days to
390             # see if it falls within config range.  It won't hurt to add names to files
391             # parsed.
392             dt_start = si['proc_start_dt']-timedelta(days=31)
393             # print dt_start
394         if fndt:
395             if dt_start <= fndt <= dt_end or m:
396                 raw_files.append(fn)
397                 raw_dts.append(fndt)
398     return (raw_files, raw_dts)
399
400 def which_raw(pi, raw_files, dts):
401     """Further limit file names based on configuration file timeframe """
402     (config_start_dt, config_end_dt) = get_config_dates(pi)
403
404     for idx, fn in enumerate(raw_files):
405         (fndt, granularity) = filt_datetime(os.path.basename(fn), gran=True)
406         if granularity == 4:
407             if fndt < config_start_dt:
408                 dts[idx] = config_start_dt
409             if fndt > config_end_dt:
410                 dts[idx] = config_end_dt
411
412     new_list = [raw_files[i] for i in range(len(raw_files)) \
413                      if config_start_dt <= dts[i] <= config_end_dt]
414
415     if not new_list:
416         new_list = [raw_files[i] for i in range(len(raw_files)) \
417                     if dts[i] <= config_end_dt]
418        
419     return new_list
420        
421
422 def raw2proc(proctype, platform=None, package=None, yyyy_mm=None):
423     """
424     Process data either in auto-mode or manual-mode
425
426     If auto-mode, process newest data for all platforms, all
427     sensors. Otherwise in manual-mode, process data for specified
428     platform, sensor package, and month.
429
430     :Parameters:
431        proctype : string
432            'auto' or 'manual' or 'spin'
433
434        platform : string
435            Platfrom id to process (e.g. 'bogue')
436        package : string
437            Sensor package id to process (e.g. 'adcp')
438        yyyy_mm : string
439            Year and month of data to process (e.g. '2007_07')
440
441     Examples
442     --------
443     >>> raw2proc(proctype='manual', platform='bogue', package='adcp', yyyy_mm='2007_06')
444     >>> raw2proc('manual', 'bogue', 'adcp', '2007_06')
445
446     Spin
447     ----
448     platform can be list of platforms or 'ALL'
449     package can be list packages or 'ALL'
450     yyyy_mm can be list of months, or datetime range
451    
452     >>> raw2proc('spin', ['b1','b2'], ['ctd1', 'ctd2'], ['2011_11'])
453     >>> raw2proc('spin', ['b1','b2'], ['ctd1', 'ctd2'], 'ALL')
454     >>> raw2proc('spin', ['b1','b2'], ['ctd1', 'ctd2'], [datetime(2011,11,1), datetime(2012,4,1)])
455     >>> raw2proc('spin', ['b1','b2'], 'ALL', 'ALL')
456    
457     Not a good idea but this will reprocess all the data from level0
458     >>> raw2proc('spin', 'ALL', 'ALL', 'ALL')
459          
460     """
461     print '\nStart time for raw2proc: %s\n' % start_dt.strftime("%Y-%b-%d %H:%M:%S UTC")
462
463     if proctype == 'auto':
464         print 'Processing in auto-mode, all platforms, all packages, latest data'
465         auto()
466     elif proctype == 'manual':
467         if platform and package and yyyy_mm:
468             print 'Processing in manual-mode ...'
469             print ' ...  platform id : %s' % platform
470             print ' ... package name : %s' % package
471             print ' ...        month : %s' % yyyy_mm
472             print ' ...  starting at : %s' % start_dt.strftime("%Y-%m-%d %H:%M:%S UTC")
473             manual(platform, package, yyyy_mm)
474         else:
475             print 'raw2proc: Manual operation requires platform, package, and month'
476             print "   >>> raw2proc(proctype='manual', platform='bogue', package='adcp', yyyy_mm='2007_07')"
477     elif proctype == 'spin':
478         if platform and package and yyyy_mm:
479             print 'Processing in spin-mode ...'
480             print ' ...  platform ids : %s' % platform
481             print ' ... package names : %s' % package
482             print ' ...        months : %s' % yyyy_mm
483             print ' ...   starting at : %s' % start_dt.strftime("%Y-%m-%d %H:%M:%S UTC")
484             spin_list = create_spin_list(platform, package, yyyy_mm)
485             spin(spin_list)
486         else:
487             print "raw2proc: Spin operation requires platform(s), package(s), and month(s)"
488             print "   >>> raw2proc(proctype='spin', platform='b1', package='ALL', yyyy_mm='ALL')"
489             print "   >>> raw2proc(proctype='spin', platform='ALL', package='met', yyyy_mm='2011_11')"
490             print "   >>> raw2proc('spin', ['b1','b2'], ['ctd1', 'ctd2'], [datetime(2011,11,1), datetime(2012,4,1)])"
491
492     else:
493         print 'raw2proc: requires either auto or manual operation'
494
495
496 def auto():
497     """Process all platforms, all packages, latest data
498
499     Notes
500     -----
501    
502     1. determine which platforms (all platforms with currently active
503        config files i.e. config_end_date is None
504     2. for each platform
505          get latest config
506          for each package
507            (determine process for 'latest' data) copy to new area when grabbed
508            parse recent data
509            yyyy_mm is the current month
510            load this months netcdf, if new month, create this months netcdf
511            update modified date and append new data in netcdf
512            
513     """
514     yyyy_mm = this_month()
515     months = find_months(yyyy_mm)
516     month_start_dt = months[1]
517     month_end_dt = months[2] - timedelta(seconds=1)
518
519     configs = find_active_configs(config_dir=defconfigs)
520     if configs:
521         # for each configuration
522         for cn in configs:
523             print ' ... config file : %s' % cn
524             pi = get_config(cn+'.platform_info')
525             asi = get_config(cn+'.sensor_info')
526             platform = pi['id']
527             (pi['config_start_dt'], pi['config_end_dt']) = get_config_dates(pi)
528
529             # for each sensor package
530             for package in asi.keys():
531                 try: # if package files, try next package
532                     print ' ... package name : %s' % package
533                     si = asi[package]
534                     si['proc_filename'] = '%s_%s_%s.nc' % (platform, package, yyyy_mm)
535                     ofn = os.path.join(si['proc_dir'], si['proc_filename'])
536                     si['proc_start_dt'] = month_start_dt
537                     si['proc_end_dt'] = month_end_dt
538                     if os.path.exists(ofn):
539                         # get last dt from current month file
540                         (es, units) = nc_get_time(ofn)
541                         last_dt = es2dt(es[-1])
542                         # if older than month_start_dt use it instead to only process newest data
543                         if last_dt>=month_start_dt:
544                             si['proc_start_dt'] = last_dt
545
546                     (raw_files, raw_dts) = find_raw(si, yyyy_mm)
547                     raw_files = which_raw(pi, raw_files, raw_dts)
548                     if raw_files:
549                         process(pi, si, raw_files, yyyy_mm)
550                     else:
551                         print ' ... ... NOTE: no new raw files found'
552
553                     # update latest data for SECOORA commons
554                     if 'latest_dir' in si.keys():
555                         # print ' ... ... latest : %s ' % si['latest_dir']
556                         proc2latest(pi, si, yyyy_mm)
557
558                     if 'csv_dir' in si.keys():
559                         proc2csv(pi, si, yyyy_mm)
560                 except:
561                     traceback.print_exc()
562     #
563     else:
564         print ' ... ... NOTE: No active platforms'
565
566 def spin(spin_list):
567     """ wrapper to run manual() for multiple months"""
568     for item in spin_list:
569         platform, package, yyyy_mm = item
570         raw2proc('manual',platform, package, yyyy_mm)
571
572 def manual(platform, package, yyyy_mm):
573     """Process data for specified platform, sensor package, and month
574
575     Notes
576     -----
577    
578     1. determine which configs
579     2. for each config for specific platform
580            if have package in config
581                which raw files
582     """
583     months = find_months(yyyy_mm)
584     month_start_dt = months[1]
585     month_end_dt = months[2] - timedelta(seconds=1)
586    
587     configs = find_configs(platform, yyyy_mm, config_dir=defconfigs)
588
589     if configs:
590         # for each configuration
591         for index in range(len(configs)):
592             cn = configs[index]
593             print ' ... config file : %s' % cn
594             pi = get_config(cn+'.platform_info')
595             (pi['config_start_dt'], pi['config_end_dt']) = get_config_dates(pi)
596             # month start and end dt to pi info
597             asi = get_config(cn+'.sensor_info')
598             if package in pi['packages']:
599                 si = asi[package]
600                 if si['utc_offset']:
601                     print ' ... ... utc_offset : %g (hours)' % si['utc_offset']
602                 si['proc_start_dt'] = month_start_dt
603                 si['proc_end_dt'] = month_end_dt
604                 si['proc_filename'] = '%s_%s_%s.nc' % (platform, package, yyyy_mm)
605                 ofn = os.path.join(si['proc_dir'], si['proc_filename'])
606                 (raw_files, raw_dts) = find_raw(si, yyyy_mm)
607                 # print raw_files
608                 # print raw_dts
609                 raw_files = which_raw(pi, raw_files, raw_dts)
610                 # print raw_files
611                 # print raw_dts
612                 # remove any previous netcdf file (platform_package_yyyy_mm.nc)
613                 if index==0  and os.path.exists(ofn):
614                     os.remove(ofn)
615                 # this added just in case data repeated in data files
616                 if os.path.exists(ofn):
617                     # get last dt from current month file
618                     (es, units) = nc_get_time(ofn)
619                     last_dt = es2dt(es[-1])
620                     # if older than month_start_dt use it instead to only process newest data
621                     if last_dt>=month_start_dt:
622                         si['proc_start_dt'] = last_dt
623
624                 if raw_files:
625                     process(pi, si, raw_files, yyyy_mm)
626                 else:
627                     print ' ... ... NOTE: no raw files found for %s %s for %s' % (package, platform, yyyy_mm)
628                
629             else:
630                 print ' ... ... NOTE: %s not operational on %s for %s' % (package, platform, yyyy_mm)               
631     else:
632         print ' ... ... ... NOTE: %s not operational for %s' % (platform, yyyy_mm)
633    
634
635 def process(pi, si, raw_files, yyyy_mm):
636     # tailored data processing for different input file formats and control over output
637     (parse, create, update) = import_processors(si['process_module'])
638     for fn in raw_files:
639         # sys.stdout.write('... %s ... ' % fn)
640         # attach file name to sensor info so parser can use it, if needed
641         si['fn'] = fn
642         lines = load_data(fn)
643         if lines:
644             data = parse(pi, si, lines)
645             # determine which index of data is within the specified timeframe (usually the month)
646             n = len(data['dt'])
647             data['in'] = numpy.array([False for i in range(n)])
648
649             for index, val in enumerate(data['dt']):
650                 if val>=pi['config_start_dt'] and \
651                        val>=si['proc_start_dt'] and \
652                        val<=si['proc_end_dt'] and \
653                        val<=pi['config_end_dt']:
654                     data['in'][index] = True
655                    
656             # if any records are in the month then write to netcdf
657             if data['in'].any():
658                 sys.stdout.write(' ... %s ... ' % fn)
659                 sys.stdout.write('%d\n' % len(data['in'].nonzero()[0]))
660                 ofn = os.path.join(si['proc_dir'], si['proc_filename'])
661                 # update or create netcdf
662                 if os.path.exists(ofn):
663                     ut = update(pi,si,data)
664                     nc_update(ofn, ut)
665                 else:
666                     ct = create(pi,si,data)
667                     nc_create(ofn, ct)
668         else:
669             # if no lines, file was empty
670             print " ... skipping file %s" % (fn,)
671
672    
673 # globals
674 start_dt = datetime.utcnow()
675 start_dt.replace(microsecond=0)
676
677 if __name__ == "__main__":
678     import optparse
679     raw2proc('auto')
680
681     # for testing
682     # proctype='manual'; platform='bogue'; package='adcp'; yyyy_mm='2007_07'
683     # raw2proc(proctype='manual', platform='bogue', package='adcp', yyyy_mm='2007_07')
Note: See TracBrowser for help on using the browser.