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

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

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

Robust Observation class and complete test coverage.

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 class MainData(list):
13     """
14     Contain data from a Scintec sodar .mnd file.
15    
16     The data is contained as a list of Profile objects
17     along with some attributes which apply to all the Profile objects.
18    
19     Parse a known good .mnd file:
20     >>> main_data = MainData(good_mnd)
21    
22     Parse the format header:
23     >>> len(main_data._format_header_spec)
24     4
25     >>> main_data._format_header_spec[0]
26     'FORMAT-1'
27    
28     Parse the file header after the format header:
29     >>> len(main_data._file_header_body)
30     26
31     >>> main_data._file_header_body[1]
32     '# file information'
33    
34     Parse the profile data:
35     >>> len(main_data)
36     48
37    
38     Parse the first profile:
39     >>> len(main_data[0])
40     39
41     >>> main_data[0].timestamp
42     '2009-11-17 00:30:00 00:30:00'
43     >>> main_data[0].variables
44     ['z', 'speed', 'dir', 'W', 'sigW', 'bck', 'error']
45     >>> main_data[0][0] == {'z':'10', 'speed':'99.99', 'dir':'999.9',
46     ...                     'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
47     ...                     'error':'0'}
48     True
49     >>> main_data[0][-1] == {'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    
54     Parse the last profile:
55     >>> len(main_data[-1])
56     39
57     >>> main_data[-1].timestamp
58     '2009-11-18 00:00:00 00:30:00'
59     >>> main_data[-1].variables
60     ['z', 'speed', 'dir', 'W', 'sigW', 'bck', 'error']
61     >>> main_data[-1][0] == {'z':'10', 'speed':'99.99', 'dir':'999.9',
62     ...                     'W':'-0.32', 'sigW':'99.99', 'bck':'9.99E+37',
63     ...                     'error':'0'}
64     True
65     >>> main_data[-1][-1] == {'z':'200', 'speed':'15.05', 'dir':'71.8',
66     ...                      'W':'-0.19', 'sigW':'0.53', 'bck':'9.99E+37',
67     ...                      'error':'0'}
68     True
69     """
70    
71     def __init__(self, mnd, *args):
72         """
73         Parse main daily Scintec sodar .mnd file.
74        
75         MainData(mnd[,file_name[,file_path]]) -> <MainData object>
76        
77         Where:
78          
79             mnd is a str object containing the complete contents read from a
80             Scintec .mnd daily sodar file including all line endings,
81            
82             file_name is an optional str object representing a file name for
83             a file which contains the referenced .mnd daily sodar file,
84            
85             file_path is an optional str object representing the path to
86             file_name.
87        
88         Parse a known good .mnd file:
89        
90         >>> main_data = MainData(good_mnd)
91         >>> main_data = MainData(good_mnd,good_name)
92         >>> main_data.file_name == good_name
93         True
94         >>> main_data = MainData(good_mnd,good_name,good_path)
95         >>> main_data.file_name == good_name
96         True
97         >>> main_data.file_path == good_path
98         True
99         """
100        
101         super(MainData, self).__init__()
102        
103         self.file_name = ''
104         self.file_path = ''
105        
106         # Optional args: smoke 'em if ya got 'em.
107         try:
108             self.file_name = str(args[0])
109             self.file_path = str(args[1])
110         except IndexError:
111             pass
112        
113         # Divide the data into blocks
114         # across boundaries separated by blank lines.
115         self._blocks = [self._block.strip()
116                         for self._block
117                         in mnd.split('\n\n')
118                         if self._block.strip()]
119        
120         # The first block is the specification of the format header.
121         # Divide it into lines.
122         self._format_header_spec = [self._line.strip()
123                                     for self._line
124                                     in self._blocks[0].split('\n')
125                                     if self._line.strip()]
126        
127         # The second block is the body of the format header.
128         # Divide it into lines.
129         self._file_header_body = [self._line.strip()
130                                   for self._line
131                                   in self._blocks[1].split('\n')
132                                   if self._line.strip()]
133        
134         # All the remaing blocks are individual profiles
135         # in chronological order.
136         self.extend([Profile([self._line.strip()
137                               for self._line
138                               in self._block.split('\n')
139                               if self._line.strip()])
140                      for self._block
141                      in self._blocks[2:]])
142        
143         # No need to haul around bookkeeping storage used for initialization.
144         del self._blocks, self._block, self._line
145
146 class Profile(list):
147     """
148     Contain data for single profile from a Scintec sodar .mnd file.
149    
150     The data is contained as a list of Observation objects
151     along with some attributes which apply to all the Profile objects.
152    
153     Parse a known good profile block:
154     >>> profile = Profile(good_profile)
155     >>> profile.timestamp
156     '2009-11-17 00:30:00 00:30:00'
157     >>> profile.variables
158     ['z', 'speed', 'dir', 'W', 'sigW', 'bck', 'error']
159     >>> profile[0] == {'z':'10', 'speed':'99.99', 'dir':'999.9',
160     ...                'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
161     ...                'error':'0'}
162     True
163     >>> profile[-1] == {'z':'200', 'speed':'99.99', 'dir':'999.9',
164     ...                 'W':'-0.07', 'sigW':'99.99', 'bck':'9.99E+37',
165     ...                 'error':'0'}
166     True
167     """
168    
169     def __init__(self, profile_block):
170         """
171         Parse a profile block from a main daily Scintec sodar .mnd file.
172        
173         Profile(profile_data) -> <Profile object>
174        
175         Where:
176          
177             profile_block is a list of str objects containing all the lines
178             from a single profile in a Scintec .mnd daily sodar file.
179
180         Parse a known good profile block:
181         >>> profile = Profile(good_profile)
182         >>> profile.timestamp
183         '2009-11-17 00:30:00 00:30:00'
184         >>> profile.variables
185         ['z', 'speed', 'dir', 'W', 'sigW', 'bck', 'error']
186         >>> profile[0] == {'z':'10', 'speed':'99.99', 'dir':'999.9',
187         ...                'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
188         ...                'error':'0'}
189         True
190         >>> profile[-1] == {'z':'200', 'speed':'99.99', 'dir':'999.9',
191         ...                 'W':'-0.07', 'sigW':'99.99', 'bck':'9.99E+37',
192         ...                 'error':'0'}
193         True
194         """
195        
196         super(Profile, self).__init__()
197
198         # The first line in the profile block is the timestamp.       
199         self.timestamp = profile_block[0]
200        
201         # The second line in the profile block is
202         # a commented list of variable names
203         self.variables = profile_block[1].split()[1:]
204        
205         # All the remaining lines in the profile block are
206         # individual obervation columns in the same order
207         # as the variable names.
208         self.extend([Observation(observation.split(),self.variables)
209                      for observation in profile_block[2:]])
210
211 class Observation(dict):
212     """
213     Contain data for single observation from a Scintec sodar .mnd files.
214    
215     The data is contained as a dictionary of variable name/value pairs.
216    
217     The variable values may also be accessed as attributes of the instance.
218    
219     Parse a known good observation:
220     >>> observation = Observation(good_observation,good_variables)
221     >>> observation == {'z':'10', 'speed':'99.99', 'dir':'999.9',
222     ...                 'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
223     ...                 'error':'0'}
224     True
225     >>> observation.z
226     '10'
227     >>> observation.speed
228     '99.99'
229     >>> observation.dir
230     '999.9'
231     >>> observation.W
232     '-0.05'
233     >>> observation.sigW
234     '0.40'
235     >>> observation.bck
236     '5.46E+03'
237     >>> observation.error
238     '0'
239     """
240    
241     def __init__(self,data,variables):
242         """
243         Parse an observation from a main daily Scintec sodar .mnd file.
244        
245         Observation(data,variables) -> <Observation object>
246        
247         Where:
248          
249             data is a list of str object representing variable values,
250            
251             variables is a list of str objects representing variable labels.
252        
253         Parse a known good observation:
254         >>> observation = Observation(good_observation,good_variables)
255         >>> observation == {'z':'10', 'speed':'99.99', 'dir':'999.9',
256         ...                 'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
257         ...                 'error':'0'}
258         True
259         >>> extra_variables = good_variables + ['extra']
260         >>> observation = Observation(good_observation,extra_variables)
261         Traceback (most recent call last):
262             ...
263         AttributeError: Same number of attributes and values required in
264         'Observation' object
265         >>> extra_observation = good_observation + ['extra']
266         >>> observation = Observation(extra_observation,good_variables)
267         Traceback (most recent call last):
268             ...
269         AttributeError: Same number of attributes and values required in
270         'Observation' object
271         """
272        
273         super(self.__class__, self).__init__()
274        
275         if len(variables) == len(data):
276             # And update the dictionary with the variable names/values.
277             super(self.__class__, self).update(dict(zip(variables,data)))
278         else:
279             # Unless there are an unequal number of variable names/values.
280             raise AttributeError, ' '.join(['Same number of attributes',
281                                             'and values required in',
282                                             repr(self.__class__.__name__),
283                                             'object',])
284    
285     def __getattr__(self,name):
286         """
287         Get a variable value by name.
288        
289         observation_instance.variable_name -> variable_value
290        
291         >>> observation = Observation(good_observation,good_variables)
292         >>> observation.z
293         '10'
294         >>> observation.speed
295         '99.99'
296         >>> observation.dir
297         '999.9'
298         >>> observation.W
299         '-0.05'
300         >>> observation.sigW
301         '0.40'
302         >>> observation.bck
303         '5.46E+03'
304         >>> observation.error
305         '0'
306         >>> observation.y
307         Traceback (most recent call last):
308             ...
309         AttributeError: 'Observation' object has no attribute 'y'
310         >>> observation.y = 'OK'
311         >>> observation.y
312         'OK'
313         >>> observation == {'z':'10', 'speed':'99.99', 'dir':'999.9',
314         ...                 'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
315         ...                 'error':'0'}
316         True
317         """
318        
319         # Attempt attribute access by dictionary key.
320         if name in self:
321             return self[name]
322         else:
323             raise AttributeError, ' '.join([repr(self.__class__.__name__),
324                                             'object has no attribute',
325                                             repr(name),])
326    
327     def __setattr__(self,name,value):
328         """
329         Protect variable values from overwriting.
330        
331         observation_instance.variable_name = variable_value
332        
333         But retrieve any other bound attribute values.
334
335         >>> observation = Observation(good_observation,good_variables)
336         >>> observation.z = 'OK'
337         Traceback (most recent call last):
338             ...
339         AttributeError: Rebinding attribute 'z' disallowed in
340         'Observation' object
341         >>> observation.y = 'OK'
342         >>> observation.y
343         'OK'
344         >>> observation == {'z':'10', 'speed':'99.99', 'dir':'999.9',
345         ...                 'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
346         ...                 'error':'0'}
347         True
348         """
349        
350         if name in self:
351             # Protect observation variables
352             raise AttributeError, ' '.join(['Rebinding attribute',
353                                             repr(name),
354                                             'disallowed in',
355                                             repr(self.__class__.__name__),
356                                             'object',])
357         else:
358             # Otherwise, do it the normal way.
359             super(self.__class__, self).__setattr__(name,value)
360    
361     def __delattr__(self,name):
362         """
363         Protect variable values from deletion.
364        
365         del observation_instance.variable_name
366        
367         But delete any other bound attribute values.
368
369         >>> observation = Observation(good_observation,good_variables)
370         >>> del observation.z
371         Traceback (most recent call last):
372             ...
373         AttributeError: Unbinding attribute 'z' disallowed in
374         'Observation' object
375         >>> del observation.y
376         Traceback (most recent call last):
377             ...
378         AttributeError: 'Observation' object has no attribute 'y'
379         >>> observation.y = 'OK'
380         >>> del observation.y
381         >>> del observation.y
382         Traceback (most recent call last):
383             ...
384         AttributeError: y
385         >>> observation == {'z':'10', 'speed':'99.99', 'dir':'999.9',
386         ...                 'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
387         ...                 'error':'0'}
388         True
389         """
390        
391         if name in self:
392             # Protect observation variables
393             raise AttributeError, ' '.join(['Unbinding attribute',
394                                             repr(name),
395                                             'disallowed in',
396                                             repr(self.__class__.__name__),
397                                             'object',])
398         else:
399             # Otherwise, do it the normal way.
400             super(self.__class__, self).__delattr__(name)
401    
402     def __setitem__(self,key,value):
403         """
404         Protect items.
405        
406         observation_instance[variable_name] = variable_value
407        
408         >>> observation = Observation(good_observation,good_variables)
409         >>> observation['z'] = 'OK'
410         Traceback (most recent call last):
411             ...
412         TypeError: 'Observation' object's items are read-only
413         >>> observation['y'] = 'OK'
414         Traceback (most recent call last):
415             ...
416         TypeError: 'Observation' object's items are read-only
417         >>> observation == {'z':'10', 'speed':'99.99', 'dir':'999.9',
418         ...                 'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
419         ...                 'error':'0'}
420         True
421         """
422        
423         raise TypeError, ' '.join([repr(self.__class__.__name__),
424                                    'object\'s items are read-only',])
425    
426     def __delitem__(self,key):
427         """
428         Protect items.
429        
430         observation_instance[variable_name] = variable_value
431        
432         >>> observation = Observation(good_observation,good_variables)
433         >>> del observation['z']
434         Traceback (most recent call last):
435             ...
436         TypeError: 'Observation' object's items are read-only
437         >>> del observation['y']
438         Traceback (most recent call last):
439             ...
440         TypeError: 'Observation' object's items are read-only
441         >>> observation == {'z':'10', 'speed':'99.99', 'dir':'999.9',
442         ...                 'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
443         ...                 'error':'0'}
444         True
445         """
446        
447         raise TypeError, ' '.join([repr(self.__class__.__name__),
448                                    'object\'s items are read-only',])
449    
450     def clear(self):
451         """
452         Protect items.
453        
454         >>> observation = Observation(good_observation,good_variables)
455         >>> observation.clear()
456         Traceback (most recent call last):
457             ...
458         TypeError: 'Observation' object's items are read-only
459         >>> observation == {'z':'10', 'speed':'99.99', 'dir':'999.9',
460         ...                 'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
461         ...                 'error':'0'}
462         True
463         """
464        
465         raise TypeError, ' '.join([repr(self.__class__.__name__),
466                                    'object\'s items are read-only',])
467    
468     def pop(self,key,*args):
469         """
470         Protect items.
471        
472         >>> observation = Observation(good_observation,good_variables)
473         >>> observation.pop('z')
474         Traceback (most recent call last):
475             ...
476         TypeError: 'Observation' object's items are read-only
477         >>> observation.pop('y')
478         Traceback (most recent call last):
479             ...
480         TypeError: 'Observation' object's items are read-only
481         >>> observation.pop('y','OK')
482         Traceback (most recent call last):
483             ...
484         TypeError: 'Observation' object's items are read-only
485         >>> observation == {'z':'10', 'speed':'99.99', 'dir':'999.9',
486         ...                 'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
487         ...                 'error':'0'}
488         True
489         """
490        
491         raise TypeError, ' '.join([repr(self.__class__.__name__),
492                                    'object\'s items are read-only',])
493    
494     def popitem(self):
495         """
496         Protect items.
497        
498         >>> observation = Observation(good_observation,good_variables)
499         >>> observation.popitem()
500         Traceback (most recent call last):
501             ...
502         TypeError: 'Observation' object's items are read-only
503         >>> observation == {'z':'10', 'speed':'99.99', 'dir':'999.9',
504         ...                 'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
505         ...                 'error':'0'}
506         True
507         """
508        
509         raise TypeError, ' '.join([repr(self.__class__.__name__),
510                                    'object\'s items are read-only',])
511    
512     def setdefault(self,key,*args):
513         """
514         Protect items.
515        
516         >>> observation = Observation(good_observation,good_variables)
517         >>> observation.setdefault('z')
518         Traceback (most recent call last):
519             ...
520         TypeError: 'Observation' object's items are read-only
521         >>> observation.setdefault('y')
522         Traceback (most recent call last):
523             ...
524         TypeError: 'Observation' object's items are read-only
525         >>> observation.setdefault('y','OK')
526         Traceback (most recent call last):
527             ...
528         TypeError: 'Observation' object's items are read-only
529         >>> observation == {'z':'10', 'speed':'99.99', 'dir':'999.9',
530         ...                 'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
531         ...                 'error':'0'}
532         True
533         """
534        
535         raise TypeError, ' '.join([repr(self.__class__.__name__),
536                                    'object\'s items are read-only',])
537    
538     def update(self,*args,**kwargs):
539         """
540         Protect items.
541        
542         >>> observation = Observation(good_observation,good_variables)
543         >>> observation.update({'UP':'up','DOWN':'down',})
544         Traceback (most recent call last):
545             ...
546         TypeError: 'Observation' object's items are read-only
547         >>> observation == {'z':'10', 'speed':'99.99', 'dir':'999.9',
548         ...                 'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
549         ...                 'error':'0'}
550         True
551         >>> observation.update((('UP','up'),('DOWN','down'),))
552         Traceback (most recent call last):
553             ...
554         TypeError: 'Observation' object's items are read-only
555         >>> observation == {'z':'10', 'speed':'99.99', 'dir':'999.9',
556         ...                 'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
557         ...                 'error':'0'}
558         True
559         >>> observation.update(UP='up',DOWN='down')
560         Traceback (most recent call last):
561             ...
562         TypeError: 'Observation' object's items are read-only
563         >>> observation == {'z':'10', 'speed':'99.99', 'dir':'999.9',
564         ...                 'W':'-0.05', 'sigW':'0.40', 'bck':'5.46E+03',
565         ...                 'error':'0'}
566         True
567         """
568        
569         raise TypeError, ' '.join([repr(self.__class__.__name__),
570                                    'object\'s items are read-only',])
571
572 def _test():
573     """
574     Run module tests in script mode.
575    
576     >>> from sodar.scintec.maindata import _test
577     """
578    
579     import doctest
580     from sodar.tests import suite
581     mnd_path,mnd_file,mnd,profile,variables,observation = \
582         suite.setUpGoodMndData()
583     doctest.testmod(extraglobs=dict(good_mnd=mnd,
584                                     good_name=mnd_file,
585                                     good_path=mnd_path,
586                                     good_profile=profile,
587                                     good_variables=variables,
588                                     good_observation=observation),
589                     optionflags=doctest.NORMALIZE_WHITESPACE)
590
591 if __name__ == "__main__":
592     _test()
Note: See TracBrowser for help on using the browser.