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

root/sodar/trunk/sodar/scintec/maindata.py

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

Merge scintec-branch.

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_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_path is an optional str object representing the path to
133             a file which contains the referenced .mnd daily sodar file.
134        
135         Parse a known good .mnd file:
136         >>> main_data = MainData(good_mnd)
137         >>> main_data = MainData(good_mnd,good_path)
138         >>> main_data.path == good_path
139         True
140         """
141        
142         super(self.__class__, self).__init__()
143        
144         self.path = ''
145        
146         # Optional args: smoke 'em if ya got 'em.
147         try:
148             self.path = str(args[0])
149         except IndexError:
150             pass
151        
152         # Divide the data into blocks
153         # across boundaries separated by blank lines.
154         blocks = [block.strip()
155                   for block
156                   in mnd.split('\n\n')
157                   if block.strip()]
158        
159         # The first block is the specification of the format header.
160         # Divide it into lines.
161         format_header_spec = [line.strip()
162                               for line
163                               in blocks[0].split('\n')
164                               if line.strip()]
165                              
166         self.format = format_header_spec[0]
167        
168         timestamp = format_header_spec[1]
169         date, first, file_count = timestamp.split()
170         date_year, date_month, date_day = date.split('-')
171         first_hour, first_minute, first_second = first.split(':')
172         self.first = datetime(int(date_year),
173                               int(date_month),
174                               int(date_day),
175                               int(first_hour),
176                               int(first_minute), # omit optional microseconds
177                               int(first_second)) # and tzinfo
178         self.file_count = int(file_count)
179        
180         self.instrument_type = format_header_spec[2]
181        
182         comment_count, variable_count, elevation_count = \
183             format_header_spec[3].split()
184         self.comment_count = int(comment_count)
185         self.variable_count = int(variable_count)
186         self.elevation_count = int(elevation_count)
187        
188         # The second block is the body of the format header.
189         # Divide it into lines.
190         file_header_body = [line.strip()
191                             for line
192                             in blocks[1].split('\n')
193                             if not line.strip().startswith('#')]
194         self.comments = dict([(name.strip(),value.strip())
195                                for line
196                                in file_header_body[0:self.comment_count]
197                                    for name,value
198                                    in (line.split(':'),)])
199         self.file_type = file_header_body[self.comment_count]
200         self.variables = [{'label':label.strip(),
201                            'symbol':symbol.strip(),
202                            'units':units.strip(),
203                            'type':vtype.strip(),
204                            'mask':mask.strip(),
205                            'gap':gap.strip(),}
206                           for line
207                           in file_header_body[self.comment_count + 1:-1]
208                               for label,symbol,units,vtype,mask,gap
209                               in (line.split('#'),)]
210         self.error = [{'label':label.strip(),
211                        'bits':bits.strip(),
212                        'mask':mask.strip(),}
213                       for label,bits,units,vtype,mask
214                       in (file_header_body[-1].split('#'),)][0]
215                        
216        
217         # All the remaing blocks are individual profiles
218         # in chronological order.
219         self.extend([Profile([line.strip()
220                               for line
221                               in block.split('\n')
222                               if line.strip()])
223                      for block
224                      in blocks[2:]])
225    
226     def __call__(self,timestamp,start=False):
227         """
228         Get a profile by timestamp
229        
230         MainData_instance(timestamp[,start]) -> <Profile object>
231        
232         Where:
233        
234             timestamp is a datetime.datetime object representing either
235             the start or stop timestamp of the Profile object returned.
236            
237             start is a bool object. If start is False (default), then
238             timestamp is matched to the stop timestamp of the returned
239             Profile object. Otherwise, if start is True, then timestamp is
240             matched to the start timestamp of the returned Profile object.
241        
242         Access profiles by timestamp:
243         >>> main_data = MainData(good_mnd)
244         >>> main_data(datetime(2009, 11, 17, 0, 30)).stop
245         datetime.datetime(2009, 11, 17, 0, 30)
246         >>> main_data(datetime(2009, 11, 17, 0, 0),True).start
247         datetime.datetime(2009, 11, 17, 0, 0)
248         >>> main_data(datetime(2009, 11, 18, 0, 0)).stop
249         datetime.datetime(2009, 11, 18, 0, 0)
250         >>> main_data(datetime(2009, 11, 17, 23, 0),True).start
251         datetime.datetime(2009, 11, 17, 23, 0)
252         >>> main_data(datetime(2009, 11, 16, 0, 30))
253         Traceback (most recent call last):
254            ...
255         ValueError: Timestamp datetime.datetime(2009, 11, 16, 0, 30)
256         not found in 'MainData' object
257         >>> main_data('OK')
258         Traceback (most recent call last):
259             ...
260         ValueError: Timestamp 'OK' not found in 'MainData' object
261         """
262        
263         for profile in self:
264             if start:
265                 boundary = profile.start
266             else:
267                 boundary = profile.stop
268             if boundary == timestamp:
269                 return profile
270         raise ValueError, ' '.join(['Timestamp',
271                                     repr(timestamp),
272                                     'not found in',
273                                     repr(self.__class__.__name__),
274                                     'object'])
275
276 class Profile(list):
277     """
278     Contain data for single profile from a Scintec sodar .mnd file.
279    
280     The data is contained as a list of Observation objects
281     along with some attributes which apply to all the Profile objects.
282    
283     Parse a known good profile block:
284     >>> profile = Profile(good_profile)
285    
286     Access the timestamp attributes:
287     >>> profile.start
288     datetime.datetime(2009, 11, 17, 0, 0)
289     >>> profile.stop
290     datetime.datetime(2009, 11, 17, 0, 30)
291    
292     Access the variable list:
293     >>> profile.variables
294     ['z', 'speed', 'dir', 'W', 'sigW', 'bck', 'error']
295    
296     Access observations by sequence number:
297     >>> profile[0] == {'z':'10', 'speed':'99.99', 'dir':'999.9',
298     ...                'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
299     ...                'error':'0'}
300     True
301     >>> profile[-1] == {'z':'200', 'speed':'99.99', 'dir':'999.9',
302     ...                 'W':'-0.07', 'sigW':'99.99', 'bck':'9.99E+37',
303     ...                 'error':'0'}
304     True
305    
306     Access observations by elevation:
307     >>> profile(10) == {'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(200) == {'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    
317     def __init__(self, profile_block):
318         """
319         Parse a profile block from a main daily Scintec sodar .mnd file.
320        
321         Profile(profile_data) -> <Profile object>
322        
323         Where:
324        
325             profile_block is a list of str objects containing all the lines
326             from a single profile in a Scintec .mnd daily sodar file.
327        
328         Parse a known good profile block:
329         >>> profile = Profile(good_profile)
330        
331         Access the timestamp attributes:
332         >>> profile.start
333         datetime.datetime(2009, 11, 17, 0, 0)
334         >>> profile.stop
335         datetime.datetime(2009, 11, 17, 0, 30)
336        
337         Access the variable list:
338         >>> profile.variables
339         ['z', 'speed', 'dir', 'W', 'sigW', 'bck', 'error']
340        
341         Access observations by sequence number:
342         >>> profile[0] == {'z':'10', 'speed':'99.99', 'dir':'999.9',
343         ...                'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
344         ...                'error':'0'}
345         True
346         >>> profile[-1] == {'z':'200', 'speed':'99.99', 'dir':'999.9',
347         ...                 'W':'-0.07', 'sigW':'99.99', 'bck':'9.99E+37',
348         ...                 'error':'0'}
349         True
350         >>> profile[100000]
351         Traceback (most recent call last):
352             ...
353         IndexError: list index out of range
354         """
355        
356         super(self.__class__, self).__init__()
357
358         # The first line in the profile block is the timestamp.       
359         timestamp = profile_block[0]
360         date, stop, duration = timestamp.split()
361         date_year, date_month, date_day = date.split('-')
362         stop_hour, stop_minute, stop_second = stop.split(':')
363         duration_hour, duration_minute, duration_second = duration.split(':')
364         interval = timedelta(0,                    # days
365                              int(duration_second),
366                              0,                    # microseconds
367                              0,                    # milliseconds
368                              int(duration_minute),
369                              int(duration_hour))   # omit optional weeks
370         self.stop = datetime(int(date_year),
371                              int(date_month),
372                              int(date_day),
373                              int(stop_hour),
374                              int(stop_minute),     # omit optional microseconds
375                              int(stop_second))     # and tzinfo
376         self.start = self.stop - interval
377        
378         # The second line in the profile block is
379         # a commented list of variable names
380         self.variables = profile_block[1].split()[1:]
381        
382         # All the remaining lines in the profile block are
383         # individual obervation columns in the same order
384         # as the variable names.
385         super(self.__class__, self).extend([Observation(observation.split(),
386                                                         self.variables)
387                                             for observation
388                                             in profile_block[2:]])
389    
390     def __call__(self,elevation):
391         """
392         Get an observation by elevation.
393        
394         Profile_instance(elevation) -> <Observation object>
395        
396         Where:
397        
398             elevation is a numeric or str object representing the elevation
399             (value keyed by 'z') of the Observation object.
400        
401         Access observations by elevation:
402         >>> profile = Profile(good_profile)
403         >>> profile(10) == {'z':'10', 'speed':'99.99', 'dir':'999.9',
404         ...                 'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
405         ...                 'error':'0'}
406         True
407         >>> profile(200) == {'z':'200', 'speed':'99.99', 'dir':'999.9',
408         ...                  'W':'-0.07', 'sigW':'99.99', 'bck':'9.99E+37',
409         ...                  'error':'0'}
410         True
411         >>> profile(100000)
412         Traceback (most recent call last):
413             ...
414         ValueError: Elevation 100000 not found in 'Profile' object
415         >>> profile('10') == {'z':'10', 'speed':'99.99', 'dir':'999.9',
416         ...                   'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
417         ...                   'error':'0'}
418         True
419         >>> profile('200') == {'z':'200', 'speed':'99.99', 'dir':'999.9',
420         ...                    'W':'-0.07', 'sigW':'99.99', 'bck':'9.99E+37',
421         ...                    'error':'0'}
422         True
423         >>> profile('100000')
424         Traceback (most recent call last):
425             ...
426         ValueError: Elevation '100000' not found in 'Profile' object
427         """
428        
429         for observation in self:
430             if observation['z'] == str(int(elevation)):
431                 return observation
432         raise ValueError, ' '.join(['Elevation',
433                                     repr(elevation),
434                                     'not found in',
435                                     repr(self.__class__.__name__),
436                                     'object'])
437
438 class Observation(dict):
439     """
440     Contain data for single observation from a Scintec sodar .mnd files.
441    
442     The data is contained as a dictionary of variable name/value pairs.
443    
444     The variable values may also be accessed as attributes of the instance.
445    
446     Parse a known good observation:
447     >>> observation = Observation(good_observation,good_variables)
448     >>> observation == {'z':'10', 'speed':'99.99', 'dir':'999.9',
449     ...                 'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
450     ...                 'error':'0'}
451     True
452     """
453    
454     def __init__(self,data,variables):
455         """
456         Parse an observation from a main daily Scintec sodar .mnd file.
457        
458         Observation(data,variables) -> <Observation object>
459        
460         Where:
461          
462             data is a list of str object representing variable values,
463            
464             variables is a list of str objects representing variable names.
465        
466         Parse a known good observation:
467         >>> observation = Observation(good_observation,good_variables)
468         >>> observation == {'z':'10', 'speed':'99.99', 'dir':'999.9',
469         ...                 'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
470         ...                 'error':'0'}
471         True
472        
473         Raise an Exception if more variable names than values:
474         >>> extra_variables = good_variables + ['extra']
475         >>> observation = Observation(good_observation,extra_variables)
476         Traceback (most recent call last):
477             ...
478         ValueError: Same number of variable names and values required in
479         'Observation' object
480        
481         Raise an Exception if more variable values than names:
482         >>> extra_observation = good_observation + ['extra']
483         >>> observation = Observation(extra_observation,good_variables)
484         Traceback (most recent call last):
485             ...
486         ValueError: Same number of variable names and values required in
487         'Observation' object
488         """
489        
490         super(self.__class__, self).__init__()
491        
492         if len(variables) == len(data):
493             # And update the dictionary with the variable names/values.
494             super(self.__class__, self).update(dict(zip(variables,data)))
495         else:
496             # Unless there are an unequal number of variable names/values.
497             raise ValueError, ' '.join(['Same number of variable',
498                                         'names and values required in',
499                                         repr(self.__class__.__name__),
500                                         'object',])
501
502 def _test():
503     """
504     Run module tests in script mode.
505    
506     >>> from sodar.scintec.maindata import _test
507     """
508    
509     import doctest
510     from sodar.tests import suite
511     mnd,mnd_path,profile,variables,observation = \
512         suite.setUpGoodMndData()
513     doctest.testmod(extraglobs=dict(good_mnd=mnd,
514                                     good_path=mnd_path,
515                                     good_profile=profile,
516                                     good_variables=variables,
517                                     good_observation=observation),
518                     optionflags=doctest.NORMALIZE_WHITESPACE)
519
520 if __name__ == "__main__":
521     _test()
Note: See TracBrowser for help on using the browser.