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

root/sodar/branches/scintec-branch/sodar/scintec/maindata.py

Revision 270 (checked in by cbc, 14 years ago)

Completed maindata.py.

Line 
1 """
2 Module to handle Scintec sodar .mnd files.
3
4 >>> from sodar.scintec import maindata
5 """
6
7 __author__ = 'Chris Calloway'
8 __email__ = 'cbc@chriscalloway.org'
9 __copyright__ = 'Copyright 2009 UNC-CH Department of Marine Science'
10 __license__ = 'GPL2'
11
12 from datetime import datetime, timedelta
13
14 class MainData(list):
15     """
16     Contain data from a Scintec sodar .mnd file.
17     
18     The data is contained as a list of Profile objects
19     along with some attributes which apply to all the Profile objects.
20     
21     Parse a known good .mnd file:
22     >>> main_data = MainData(good_mnd)
23     >>> len(main_data)
24     48
25     >>> main_data.format
26     'FORMAT-1'
27     >>> main_data.first
28     datetime.datetime(2009, 11, 17, 0, 30)
29     >>> main_data.file_count
30     0
31     >>> main_data.comment_count
32     6
33     >>> main_data.variable_count
34     6
35     >>> main_data.elevation_count
36     39
37     >>> main_data.comments == {'device serial number':'A-F-0050',
38     ...                        'station code':'billymitchell',
39     ...                        'software version':'APRun 1.31',
40     ...                        'antenna azimuth angle [deg]':'0',
41     ...                        'height above ground [m]':'0',
42     ...                        'height above sea level [m]':'0'}
43     True
44     >>> main_data.variables == [{'label':'height','symbol':'z',
45     ...                              'units':'m','type':'Z1',
46     ...                              'mask':'0','gap':'99999',},
47     ...                         {'label':'wind speed','symbol':'speed',
48     ...                              'units':'m/s','type':'G1',
49     ...                              'mask':'0','gap':'99.99',},
50     ...                         {'label':'wind direction','symbol':'dir',
51     ...                              'units':'deg','type':'R1',
52     ...                              'mask':'0','gap':'999.9',},
53     ...                         {'label':'wind W','symbol':'W',
54     ...                              'units':'m/s','type':'S',
55     ...                              'mask':'0','gap':'99.99',},
56     ...                         {'label':'sigma W','symbol':'sigW',
57     ...                              'units':'m/s','type':'S',
58     ...                              'mask':'0','gap':'99.99',},
59     ...                         {'label':'backscatter','symbol':'bck',
60     ...                              'units':'','type':'S',
61     ...                              'mask':'0','gap':'9.99E+37',},]
62     True
63     >>> main_data.error == {'label':'error code',
64     ...                 'bits':'- - - - - - - - groundclutter - - - - - - -',
65     ...                 'mask':'IIIIIIIIWIIIIIII',}
66     True
67     
68     Parse the first profile:
69     >>> len(main_data[0])
70     39
71     >>> main_data(datetime(2009, 11, 17, 0, 30)).stop
72     datetime.datetime(2009, 11, 17, 0, 30)
73     >>> main_data(datetime(2009, 11, 17, 0, 0),True).start
74     datetime.datetime(2009, 11, 17, 0, 0)
75     >>> main_data[0].variables
76     ['z', 'speed', 'dir', 'W', 'sigW', 'bck', 'error']
77     >>> main_data[0][0] == {'z':'10', 'speed':'99.99', 'dir':'999.9',
78     ...                     'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
79     ...                     'error':'0'}
80     True
81     >>> main_data[0](10) == {'z':'10', 'speed':'99.99', 'dir':'999.9',
82     ...                      'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
83     ...                      'error':'0'}
84     True
85     >>> main_data[0][-1] == {'z':'200', 'speed':'99.99', 'dir':'999.9',
86     ...                      'W':'-0.07', 'sigW':'99.99', 'bck':'9.99E+37',
87     ...                      'error':'0'}
88     True
89     >>> main_data[0](200) == {'z':'200', 'speed':'99.99', 'dir':'999.9',
90     ...                       'W':'-0.07', 'sigW':'99.99', 'bck':'9.99E+37',
91     ...                       'error':'0'}
92     True
93     
94     Parse the last profile:
95     >>> len(main_data[-1])
96     39
97     >>> main_data(datetime(2009, 11, 18, 0, 0)).stop
98     datetime.datetime(2009, 11, 18, 0, 0)
99     >>> main_data(datetime(2009, 11, 17, 23, 0),True).start
100     datetime.datetime(2009, 11, 17, 23, 0)
101     >>> main_data[-1].variables
102     ['z', 'speed', 'dir', 'W', 'sigW', 'bck', 'error']
103     >>> main_data[-1][0] == {'z':'10', 'speed':'99.99', 'dir':'999.9',
104     ...                      'W':'-0.32', 'sigW':'99.99', 'bck':'9.99E+37',
105     ...                      'error':'0'}
106     True
107     >>> main_data[-1](10) == {'z':'10', 'speed':'99.99', 'dir':'999.9',
108     ...                       'W':'-0.32', 'sigW':'99.99', 'bck':'9.99E+37',
109     ...                       'error':'0'}
110     True
111     >>> main_data[-1][-1] == {'z':'200', 'speed':'15.05', 'dir':'71.8',
112     ...                       'W':'-0.19', 'sigW':'0.53', 'bck':'9.99E+37',
113     ...                       'error':'0'}
114     True
115     >>> main_data[-1](200) == {'z':'200', 'speed':'15.05', 'dir':'71.8',
116     ...                        'W':'-0.19', 'sigW':'0.53', 'bck':'9.99E+37',
117     ...                        'error':'0'}
118     True
119     """
120    
121     def __init__(self, mnd, *args):
122         """
123         Parse main daily Scintec sodar .mnd file.
124         
125         MainData(mnd[,file_name[,file_path]]) -> <MainData object>
126         
127         Where:
128         
129             mnd is a str object containing the complete contents read from a
130             Scintec .mnd daily sodar file including all line endings,
131             
132             file_name is an optional str object representing a file name for
133             a file which contains the referenced .mnd daily sodar file,
134             
135             file_path is an optional str object representing the path to
136             file_name.
137         
138         Parse a known good .mnd file:
139         >>> main_data = MainData(good_mnd)
140         >>> main_data = MainData(good_mnd,good_name)
141         >>> main_data.file_name == good_name
142         True
143         >>> main_data = MainData(good_mnd,good_name,good_path)
144         >>> main_data.file_name == good_name
145         True
146         >>> main_data.file_path == good_path
147         True
148         """
149        
150         super(self.__class__, self).__init__()
151        
152         self.file_name = ''
153         self.file_path = ''
154        
155         # Optional args: smoke 'em if ya got 'em.
156         try:
157             self.file_name = str(args[0])
158             self.file_path = str(args[1])
159         except IndexError:
160             pass
161        
162         # Divide the data into blocks
163         # across boundaries separated by blank lines.
164         blocks = [block.strip()
165                   for block
166                   in mnd.split('\n\n')
167                   if block.strip()]
168        
169         # The first block is the specification of the format header.
170         # Divide it into lines.
171         format_header_spec = [line.strip()
172                               for line
173                               in blocks[0].split('\n')
174                               if line.strip()]
175                              
176         self.format = format_header_spec[0]
177        
178         timestamp = format_header_spec[1]
179         date, first, file_count = timestamp.split()
180         date_year, date_month, date_day = date.split('-')
181         first_hour, first_minute, first_second = first.split(':')
182         self.first = datetime(int(date_year),
183                               int(date_month),
184                               int(date_day),
185                               int(first_hour),
186                               int(first_minute), # omit optional microseconds
187                               int(first_second)) # and tzinfo
188         self.file_count = int(file_count)
189        
190         self.instrument_type = format_header_spec[2]
191        
192         comment_count, variable_count, elevation_count = \
193             format_header_spec[3].split()
194         self.comment_count = int(comment_count)
195         self.variable_count = int(variable_count)
196         self.elevation_count = int(elevation_count)
197        
198         # The second block is the body of the format header.
199         # Divide it into lines.
200         file_header_body = [line.strip()
201                             for line
202                             in blocks[1].split('\n')
203                             if not line.strip().startswith('#')]
204         self.comments = dict([(name.strip(),value.strip())
205                                for line
206                                in file_header_body[0:self.comment_count]
207                                    for name,value
208                                    in (line.split(':'),)])
209         self.file_type = file_header_body[self.comment_count]
210         self.variables = [{'label':label.strip(),
211                            'symbol':symbol.strip(),
212                            'units':units.strip(),
213                            'type':vtype.strip(),
214                            'mask':mask.strip(),
215                            'gap':gap.strip(),}
216                           for line
217                           in file_header_body[self.comment_count + 1:-1]
218                               for label,symbol,units,vtype,mask,gap
219                               in (line.split('#'),)]
220         self.error = [{'label':label.strip(),
221                        'bits':bits.strip(),
222                        'mask':mask.strip(),}
223                       for label,bits,units,vtype,mask
224                       in (file_header_body[-1].split('#'),)][0]
225                        
226        
227         # All the remaing blocks are individual profiles
228         # in chronological order.
229         self.extend([Profile([line.strip()
230                               for line
231                               in block.split('\n')
232                               if line.strip()])
233                      for block
234                      in blocks[2:]])
235    
236     def __call__(self,timestamp,start=False):
237         """
238         Get a profile by timestamp
239         
240         MainData_instance(timestamp[,start]) -> <Profile object>
241         
242         Where:
243         
244             timestamp is a datetime.datetime object representing either
245             the start or stop timestamp of the Profile object returned.
246             
247             start is a bool object. If start is False (default), then
248             timestamp is matched to the stop timestamp of the returned
249             Profile object. Otherwise, if start is True, then timestamp is
250             matched to the start timestamp of the returned Profile object.
251         
252         Access profiles by timestamp:
253         >>> main_data = MainData(good_mnd)
254         >>> main_data(datetime(2009, 11, 17, 0, 30)).stop
255         datetime.datetime(2009, 11, 17, 0, 30)
256         >>> main_data(datetime(2009, 11, 17, 0, 0),True).start
257         datetime.datetime(2009, 11, 17, 0, 0)
258         >>> main_data(datetime(2009, 11, 18, 0, 0)).stop
259         datetime.datetime(2009, 11, 18, 0, 0)
260         >>> main_data(datetime(2009, 11, 17, 23, 0),True).start
261         datetime.datetime(2009, 11, 17, 23, 0)
262         >>> main_data(datetime(2009, 11, 16, 0, 30))
263         Traceback (most recent call last):
264            ...
265         ValueError: Timestamp datetime.datetime(2009, 11, 16, 0, 30)
266         not found in 'MainData' object
267         >>> main_data('OK')
268         Traceback (most recent call last):
269             ...
270         ValueError: Timestamp 'OK' not found in 'MainData' object
271         """
272        
273         for profile in self:
274             if start:
275                 boundary = profile.start
276             else:
277                 boundary = profile.stop
278             if boundary == timestamp:
279                 return profile
280         raise ValueError, ' '.join(['Timestamp',
281                                     repr(timestamp),
282                                     'not found in',
283                                     repr(self.__class__.__name__),
284                                     'object'])
285
286 class Profile(list):
287     """
288     Contain data for single profile from a Scintec sodar .mnd file.
289     
290     The data is contained as a list of Observation objects
291     along with some attributes which apply to all the Profile objects.
292     
293     Parse a known good profile block:
294     >>> profile = Profile(good_profile)
295     
296     Access the timestamp attributes:
297     >>> profile.start
298     datetime.datetime(2009, 11, 17, 0, 0)
299     >>> profile.stop
300     datetime.datetime(2009, 11, 17, 0, 30)
301     
302     Access the variable list:
303     >>> profile.variables
304     ['z', 'speed', 'dir', 'W', 'sigW', 'bck', 'error']
305     
306     Access observations by sequence number:
307     >>> profile[0] == {'z':'10', 'speed':'99.99', 'dir':'999.9',
308     ...                'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
309     ...                'error':'0'}
310     True
311     >>> profile[-1] == {'z':'200', 'speed':'99.99', 'dir':'999.9',
312     ...                 'W':'-0.07', 'sigW':'99.99', 'bck':'9.99E+37',
313     ...                 'error':'0'}
314     True
315     
316     Access observations by elevation:
317     >>> profile(10) == {'z':'10', 'speed':'99.99', 'dir':'999.9',
318     ...                 'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
319     ...                 'error':'0'}
320     True
321     >>> profile(200) == {'z':'200', 'speed':'99.99', 'dir':'999.9',
322     ...                  'W':'-0.07', 'sigW':'99.99', 'bck':'9.99E+37',
323     ...                  'error':'0'}
324     True
325     """
326    
327     def __init__(self, profile_block):
328         """
329         Parse a profile block from a main daily Scintec sodar .mnd file.
330         
331         Profile(profile_data) -> <Profile object>
332         
333         Where:
334         
335             profile_block is a list of str objects containing all the lines
336             from a single profile in a Scintec .mnd daily sodar file.
337         
338         Parse a known good profile block:
339         >>> profile = Profile(good_profile)
340         
341         Access the timestamp attributes:
342         >>> profile.start
343         datetime.datetime(2009, 11, 17, 0, 0)
344         >>> profile.stop
345         datetime.datetime(2009, 11, 17, 0, 30)
346         
347         Access the variable list:
348         >>> profile.variables
349         ['z', 'speed', 'dir', 'W', 'sigW', 'bck', 'error']
350         
351         Access observations by sequence number:
352         >>> profile[0] == {'z':'10', 'speed':'99.99', 'dir':'999.9',
353         ...                'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
354         ...                'error':'0'}
355         True
356         >>> profile[-1] == {'z':'200', 'speed':'99.99', 'dir':'999.9',
357         ...                 'W':'-0.07', 'sigW':'99.99', 'bck':'9.99E+37',
358         ...                 'error':'0'}
359         True
360         >>> profile[100000]
361         Traceback (most recent call last):
362             ...
363         IndexError: list index out of range
364         """
365        
366         super(self.__class__, self).__init__()
367
368         # The first line in the profile block is the timestamp.       
369         timestamp = profile_block[0]
370         date, stop, duration = timestamp.split()
371         date_year, date_month, date_day = date.split('-')
372         stop_hour, stop_minute, stop_second = stop.split(':')
373         duration_hour, duration_minute, duration_second = duration.split(':')
374         interval = timedelta(0,                    # days
375                              int(duration_second),
376                              0,                    # microseconds
377                              0,                    # milliseconds
378                              int(duration_minute),
379                              int(duration_hour))   # omit optional weeks
380         self.stop = datetime(int(date_year),
381                              int(date_month),
382                              int(date_day),
383                              int(stop_hour),
384                              int(stop_minute),     # omit optional microseconds
385                              int(stop_second))     # and tzinfo
386         self.start = self.stop - interval
387        
388         # The second line in the profile block is
389         # a commented list of variable names
390         self.variables = profile_block[1].split()[1:]
391        
392         # All the remaining lines in the profile block are
393         # individual obervation columns in the same order
394         # as the variable names.
395         super(self.__class__, self).extend([Observation(observation.split(),
396                                                         self.variables)
397                                             for observation
398                                             in profile_block[2:]])
399    
400     def __call__(self,elevation):
401         """
402         Get an observation by elevation.
403         
404         Profile_instance(elevation) -> <Observation object>
405         
406         Where:
407         
408             elevation is a numeric or str object representing the elevation
409             (value keyed by 'z') of the Observation object.
410         
411         Access observations by elevation:
412         >>> profile = Profile(good_profile)
413         >>> profile(10) == {'z':'10', 'speed':'99.99', 'dir':'999.9',
414         ...                 'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
415         ...                 'error':'0'}
416         True
417         >>> profile(200) == {'z':'200', 'speed':'99.99', 'dir':'999.9',
418         ...                  'W':'-0.07', 'sigW':'99.99', 'bck':'9.99E+37',
419         ...                  'error':'0'}
420         True
421         >>> profile(100000)
422         Traceback (most recent call last):
423             ...
424         ValueError: Elevation 100000 not found in 'Profile' object
425         >>> profile('10') == {'z':'10', 'speed':'99.99', 'dir':'999.9',
426         ...                   'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
427         ...                   'error':'0'}
428         True
429         >>> profile('200') == {'z':'200', 'speed':'99.99', 'dir':'999.9',
430         ...                    'W':'-0.07', 'sigW':'99.99', 'bck':'9.99E+37',
431         ...                    'error':'0'}
432         True
433         >>> profile('100000')
434         Traceback (most recent call last):
435             ...
436         ValueError: Elevation '100000' not found in 'Profile' object
437         """
438        
439         for observation in self:
440             if observation['z'] == str(int(elevation)):
441                 return observation
442         raise ValueError, ' '.join(['Elevation',
443                                     repr(elevation),
444                                     'not found in',
445                                     repr(self.__class__.__name__),
446                                     'object'])
447
448 class Observation(dict):
449     """
450     Contain data for single observation from a Scintec sodar .mnd files.
451     
452     The data is contained as a dictionary of variable name/value pairs.
453     
454     The variable values may also be accessed as attributes of the instance.
455     
456     Parse a known good observation:
457     >>> observation = Observation(good_observation,good_variables)
458     >>> observation == {'z':'10', 'speed':'99.99', 'dir':'999.9',
459     ...                 'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
460     ...                 'error':'0'}
461     True
462     """
463    
464     def __init__(self,data,variables):
465         """
466         Parse an observation from a main daily Scintec sodar .mnd file.
467         
468         Observation(data,variables) -> <Observation object>
469         
470         Where:
471         
472             data is a list of str object representing variable values,
473             
474             variables is a list of str objects representing variable names.
475         
476         Parse a known good observation:
477         >>> observation = Observation(good_observation,good_variables)
478         >>> observation == {'z':'10', 'speed':'99.99', 'dir':'999.9',
479         ...                 'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
480         ...                 'error':'0'}
481         True
482         
483         Raise an Exception if more variable names than values:
484         >>> extra_variables = good_variables + ['extra']
485         >>> observation = Observation(good_observation,extra_variables)
486         Traceback (most recent call last):
487             ...
488         ValueError: Same number of variable names and values required in
489         'Observation' object
490         
491         Raise an Exception if more variable values than names:
492         >>> extra_observation = good_observation + ['extra']
493         >>> observation = Observation(extra_observation,good_variables)
494         Traceback (most recent call last):
495             ...
496         ValueError: Same number of variable names and values required in
497         'Observation' object
498         """
499        
500         super(self.__class__, self).__init__()
501        
502         if len(variables) == len(data):
503             # And update the dictionary with the variable names/values.
504             super(self.__class__, self).update(dict(zip(variables,data)))
505         else:
506             # Unless there are an unequal number of variable names/values.
507             raise ValueError, ' '.join(['Same number of variable',
508                                         'names and values required in',
509                                         repr(self.__class__.__name__),
510                                         'object',])
511
512 def _test():
513     """
514     Run module tests in script mode.
515     
516     >>> from sodar.scintec.maindata import _test
517     """
518    
519     import doctest
520     from sodar.tests import suite
521     mnd_path,mnd_file,mnd,profile,variables,observation = \
522         suite.setUpGoodMndData()
523     doctest.testmod(extraglobs=dict(good_mnd=mnd,
524                                     good_name=mnd_file,
525                                     good_path=mnd_path,
526                                     good_profile=profile,
527                                     good_variables=variables,
528                                     good_observation=observation),
529                     optionflags=doctest.NORMALIZE_WHITESPACE)
530
531 if __name__ == "__main__":
532     _test()
Note: See TracBrowser for help on using the browser.