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

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

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

Refactor to simplify (no more read only nonsense).

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