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

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

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

Fix for Windows .mnd files binary copied to Unix.

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