source: nappy/trunk/nappy/nc_interface/na_to_cdms.py @ 3991

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/nappy/trunk/nappy/nc_interface/na_to_cdms.py@3991
Revision 3991, 16.6 KB checked in by astephen, 12 years ago (diff)

Converted all global_atts to lists of key,value pairs so order can be maintained.

Line 
1#   Copyright (C) 2004 CCLRC & NERC( Natural Environment Research Council ).
2#   This software may be distributed under the terms of the
3#   Q Public License, version 1.0 or later. http://ndg.nerc.ac.uk/public_docs/QPublic_license.txt
4
5"""
6na_to_cdms.py
7=============
8
9Container module for class NADictToCdmsObjects that is sub-classed
10by NAToNC classes.
11
12"""
13
14# Imports from python standard library
15import sys
16import re
17import time
18
19# Import from nappy package
20from nappy.na_error import na_error
21import nappy.utils
22import nappy.utils.common_utils
23
24na_to_nc_map = nappy.utils.getConfigDict()["na_to_nc_map"]
25
26# Import external packages (if available)
27if sys.platform.find("win") > -1:
28    raise na_error.NAPlatformError("Windows does not support CDMS. CDMS is required to convert to CDMS objects and NetCDF.")
29
30try:
31    import cdms2 as cdms
32    import numpy as N
33except:
34    try:
35        import cdms
36        import Numeric as N
37    except:
38        raise Exception("Could not import third-party software. Nappy requires the CDMS and Numeric packages to be installed to convert to CDMS and NetCDF.")
39
40
41# Define global variables
42safe_nc_id = re.compile("[\/\s\[\(\)\]\=\+\-\?\#\~\@\&\$\%\!\*\{\}\^]+")
43time_units_pattn = re.compile("\w+\s+since\s+\d{4}-\d{1,2}-\d{1,2}\s+\d+:\d+:\d+")
44max_id_length = 40
45special_comment_known_strings = ("###NASA Ames Special Comments follow###", 
46                                     "###NASA Ames Special Comments end###", 
47                                  "Additional Variable Attributes defined in the source file and not translated elsewhere:",
48                                  "Additional Global Attributes defined in the source file and not translated elsewhere:", "\n")
49
50normal_comment_known_strings = ("###NASA Ames Normal Comments follow###", 
51                                "###NASA Ames Normal Comments end###", 
52                                "###Data Section begins on the next line###",
53                                            "Additional Variable Attributes defined in the source file and not translated elsewhere:",
54                                                "Additional Global Attributes defined in the source file and not translated elsewhere:", 
55                                "\n")
56
57time_units_warning_message = """\nWARNING: Could not recognise time units. For true NetCDF compability
58please insert the correct time unit string below in the format:
59   
60    <units> since <YYYY>-<MM>-<DD> <hh>-<mm>-<ss>
61   
62Where:
63    <units> is a known time interval such as years, months, days, etc.
64    <YYYY> is the year, <MM> is the month, <DD> is the day,
65    <hh> is the hour, <mm> is minutes, <ss> is seconds.
66"""
67
68DEBUG = nappy.utils.getDebug() 
69
70
71class NADictToCdmsObjects:
72    """
73    Converts a NA File instance to a tuple of CDMS objects.
74    """
75   
76    def __init__(self, na_file_obj, variables="all", aux_variables="all",
77                 global_attributes=[("Conventions":"CF-1.0")],
78                 time_units=None, time_warning=True, 
79                 rename_variables={}):
80        """
81        Sets up instance variables.
82        """
83        self.na_file_obj = na_file_obj     
84        self.variables = variables
85        self.aux_variables = aux_variables
86        self.global_attributes = global_attributes
87        self.time_units = time_units
88        self.time_warning = time_warning
89        self.rename_variables = rename_variables
90
91        # Check if we have capability to convert this FFI
92        if self.na_file_obj.FFI in (2110, 2160, 2310): 
93                raise Exception("Cannot convert NASA Ames File Format Index (FFI) " + `self.na_file_obj.FFI` + " to CDMS objects. No mapping implemented yet.")
94
95        self.output_message = []  # for output printing message
96        self.converted = False
97
98    def convert(self):
99        """
100        Reads the NASA Ames file object and converts to CDMS objects.
101        Returns (variable_list, aux_variable_list, global_attributes_list).
102        All these can be readily written to a CDMS File object.
103        """
104        if self.converted == True:
105            print "Already converted to CDMS objects."
106            return (self.cdms_variables, self.cdms_aux_variables, self.global_attributes)
107
108        self.na_file_obj.readData()
109
110        # Convert global attribute
111        self._mapNACommentsToGlobalAttributes()
112
113        # Convert axes
114        if not hasattr(self, 'cdms_axes'):  self._convertCdmsAxes()
115
116        # Convert main variables
117        if not hasattr(self, 'cdms_variables'):  self._convertCdmsVariables()
118
119        # Then do auxiliary variables
120        if hasattr(self.na_file_obj, "NAUXV") and (type(self.na_file_obj.NAUXV) == type(1) and self.na_file_obj.NAUXV > 0):   # Are there any auxiliary variables?
121            if not hasattr(self, 'cdms_aux_variables'): 
122                self._convertCdmsAuxVariables()
123        else:
124            self.cdms_aux_variables = []
125           
126        self.converted = True
127        return (self.cdms_variables, self.cdms_aux_variables, self.global_attributes)
128
129
130    def _mapNACommentsToGlobalAttributes(self):
131        """
132        Maps the NASA Ames comments section to global attributes and append them to the
133        self.global_attributes list.
134        """
135        glob_atts = dict(self.global_attributes)
136       
137        for key in na_to_nc_map.keys():
138
139            if type(key) == type((1,2)):
140
141                if key == ("SCOM", "NCOM"):
142                    # Map special and normal comments into the global comments section
143                    comment_line = ""
144
145                    # Add Special comments first
146                    if self.na_file_obj.NSCOML > 0:
147                        comment_line += "###NASA Ames Special Comments follow###\n"
148
149                        for i in self.na_file_obj.SCOM: 
150                           
151                            if i.strip() not in special_comment_known_strings:
152                                comment_line += ("\n" + i)
153                           
154                        comment_line += "\n###NASA Ames Special Comments end###\n"
155
156                    # Now add normal comments
157                    if self.na_file_obj.NNCOML > 0:
158                        comment_line += "###NASA Ames Normal Comments follow###\n"
159
160                        for i in self.na_file_obj.NCOM: 
161                            if i.strip() not in normal_comment_known_strings:
162                                comment_line += ("\n" + i)
163
164                        comment_line += "\n###NASA Ames Normal Comments end###"
165
166                    # Tidy comment line then write to global atts dict
167                    comment_line = comment_line.replace("\n\n", "\n")
168
169                    #self.global_attributes["comment"] = comment_line
170                    glob_atts["comment"] = comment_line
171
172                elif key == ("ONAME", "ORG"):
173                    # Map the two organisation NA files to the institution field in CDMS (NetCDF)
174                    institution = "%s (ONAME from NASA Ames file); %s (ORG from NASA Ames file)." % \
175                                         (self.na_file_obj.ONAME, self.na_file_obj.ORG)
176                    #self.global_attributes["institution"] = institution
177                    glob_atts["institution"] = institution
178                    #self.cdms_file.institution = "%s (ONAME from NASA Ames file); %s (ORG from NASA Ames file)." % (self.na_file_obj.ONAME, self.na_file_obj.ORG)
179
180                else:
181                    # Any other strange tuple just gets merged into a string
182                    item = (getattr(self.na_file_obj, key[0])) + "\n" + (getattr(self.na_file_obj, key[1]))
183
184                    # setattr(self.cdms_file, cdms_map.fromNA[key], item)
185                    #self.global_attributes[na_to_nc_map[key]] = item
186                    glob_atts[na_to_nc_map[key]] = item
187
188            elif key == "RDATE":
189                # RDATE = Revision date - update this and put in history global attribute
190                date_parts = getattr(self.na_file_obj, "RDATE")
191                date_string = "%.4d-%.2d-%.2d" % tuple(date_parts)
192                hist = date_string + " - NASA Ames File created/revised.\n"
193                time_string = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time()))
194                version = nappy.utils.getVersion()
195                hist = "%s\n%s - Converted to CDMS (NetCDF) format using nappy-%s." % (hist, time_string, version)
196                # self.cdms_file.history = hist
197                print "DIDNT HAVE MAPPING SO PUT HISTORY IN LINE 160 IN na_to_cdms.py"
198                #self.global_attributes["history"] = hist
199                glob_atts["history"] = hist           
200            else:
201                # Anything else just needs to be stored as a global attribute
202                # setattr(self.cdms_file, cdms_map.fromNA[key], getattr(self, key))
203                #self.global_attributes[na_to_nc_map[key]] = getattr(self.na_file_obj, key)
204                glob_atts[na_to_nc_map[key]] = getattr(self.na_file_obj, key)
205
206        # Now remake global atts list
207        new_atts = []
208
209        for key, value in self.global_attributes:
210            new_atts.append( (key, glob_atts[key]) )
211        used_keys = [i[0] for i in new_atts]
212
213        for key in glob_atts.keys():
214            if key not in used_keys:
215                new_atts.append( (key, glob_atts[key]) )
216
217        self.global_attributes = new_atts[:]
218
219    def _convertCdmsVariables(self):
220        """
221        Creates cdms variable list for writing out.
222        """
223        self.cdms_variables = []
224
225        if self.variables in (None, "all"):   
226            for var_number in range(self.na_file_obj.NV):
227                self.cdms_variables.append(self._convertNAToCdmsVariable(var_number))
228        else:
229            if type(self.variables[0]) == type(1): # They are integers = indices
230                for var_number in self.variables:
231                    self.cdms_variables.append(self._convertNAToCdmsVariable(var_number))   
232            elif type(self.variables[0]) == type("string"):  # Vars are strings
233                for var_name in self.variables:
234                    if var_name in self.na_file_obj.VNAME:
235                        var_number = self.na_file_obj.VNAME.index(var_name)
236                        self.cdms_variables.append(self._convertNAToCdmsVariable(var_number))
237                    else:
238                        raise Exception("Variable name not known: " + var_name)
239
240
241    def _convertNAToCdmsVariable(self, var_number, attributes={}):
242        """
243        Creates a single cdms variable from the variable number provided in the list.
244        """
245        (var_name, units, miss, scal) = self.na_file_obj.getVariable(var_number)
246        msg = "\nAdding variable: %s" % self.na_file_obj.VNAME[var_number]
247        print msg
248        self.output_message.append(msg)
249        array = N.array(self.na_file_obj.V[var_number])
250        array = array * scal
251        # Set up axes
252        if not hasattr(self, 'cdms_axes'):
253            self._convertCdmsAxes()
254
255        # Set up variable
256        var=cdms.createVariable(array, axes=self.cdms_axes, fill_value=miss, attributes=attributes)
257
258        # Sort units etc
259        if units:   var.units=units
260       
261        # Add the best variable name
262        if len(var_name) < max_id_length:
263            var.id=safe_nc_id.sub("_", var_name).lower()
264        else:
265            var.id="naVariable_%s" % (var_number)
266       
267             # Check if mapping provided for renaming this variable
268        if var_name in self.rename_variables.keys():
269            var_name = self.rename_variables[var_name]
270           
271        var.long_name = var.name = var.title = var_name
272
273        # Add a NASA Ames variable number (for mapping correctly back to NASA Ames)
274        var.nasa_ames_var_number = var_number
275        return var
276
277    def _convertCdmsAuxVariables(self):
278        """
279        Creates a cdms variable from an auxiliary variable
280        """
281        self.cdms_aux_variables = []
282
283        if self.aux_variables in (None, "all"):
284            for avar_number in range(self.na_file_obj.NAUXV):
285                self.cdms_aux_variables.append(self._convertNAAuxToCdmsVariable(avar_number))
286        else:
287            if type(self.aux_variables[0]) == type(1): # They are integers = indices
288                for avar_number in self.aux_variables:
289                    self.cdms_aux_variables.append(self._convertNAAuxToCdmsVariable(avar_number))   
290
291            elif type(self.aux_variables[0]) == type("string"): # They are strings
292                for avar_name in self.aux_variables:
293                    if avar_name in self.na_file_obj.ANAME:
294                        avar_number = self.na_file_obj.ANAME.index(avar_name)
295                        self.cdms_aux_variables.append(self._convertNAAuxToCdmsVariable(avar_number)) 
296            else:
297                raise Exception("Auxiliary variable name not known: " + avar_name)         
298
299    def _convertNAAuxToCdmsVariable(self, avar_number, attributes={}):
300        """
301        Converts an auxiliary variable to a cdms variable.
302        """
303        (var_name, units, miss, scal) = self.na_file_obj.getAuxVariable(avar_number)
304        array = N.array(self.na_file_obj.A[avar_number])
305        array = array * scal
306
307        msg="\nAdding auxiliary variable: %s" % self.na_file_obj.ANAME[avar_number]
308        print msg
309        self.output_message.append(msg)
310
311        # Set up axes
312        if not hasattr(self, 'cdms_axes'):
313            self._convertCdmsAxes()
314
315        # Set up variable
316        var = cdms.createVariable(array, axes=[self.cdms_axes[0]], fill_value=miss, 
317                                  attributes=attributes)
318
319        # Sort units etc
320        if units:   var.units = units
321        if len(var_name) < max_id_length:
322            var.id = safe_nc_id.sub("_", var_name).lower()
323        else:
324            var.id = "naAuxVariable_%s" % (avar_number)
325
326            # Check if mapping provided for renaming this variable
327        if var_name in self.rename_variables.keys():
328            var_name = self.rename_variables[var_name]
329
330        var.long_name = var.name = var.title = var_name
331
332        # Add a NASA Ames auxiliary variable number (for mapping correctly back to NASA Ames)
333        var.nasa_ames_aux_var_number = avar_number
334        return var       
335
336    def _convertCdmsAxes(self): 
337        """
338        Creates cdms axes from information provided in the NASA Ames dictionary.
339        """
340        if not hasattr(self, 'cdms_axes'):       
341            self.cdms_axes = []
342
343        for ivar_number in range(self.na_file_obj.NIV):
344            self.cdms_axes.append(self._convertNAIndVarToCdmsAxis(ivar_number))
345
346
347    def _convertNAIndVarToCdmsAxis(self, ivar_number):
348        """
349        Creates a cdms axis from a NASA Ames independent variable.
350        """
351        if self.na_file_obj._normalized_X == False:   self.na_file_obj._normalizeIndVars()
352
353        if self.na_file_obj.NIV == 1:
354            array = self.na_file_obj.X
355        else:
356            array = self.na_file_obj.X[ivar_number]
357
358        axis = cdms.createAxis(array)
359        axis.id = axis.name = axis.long_name = self.na_file_obj.XNAME[ivar_number]
360        (var_name, units) = self.na_file_obj.getIndependentVariable(ivar_number)
361       
362        # Sort units etc
363        if units:   axis.units = units
364        if len(var_name) < max_id_length:
365            axis.id = safe_nc_id.sub("_", var_name).lower()
366        else:
367            axis.id = "naIndVariable_%s" % (ivar_number)
368
369        if units: axis.units = units
370
371        axis_types = ("longitude", "latitude", "level", "time")
372
373        for axis_type in axis_types:
374            if re.search(axis_type, var_name, re.IGNORECASE):
375                axis.standard_name = axis.id = axis_type
376                # Designate it CF-style if known axis type (e.g. axis.designateTime() etc..)
377                exec "axis.designate%s()" % axis_type.title()
378                print "CHECK AXIS GOT DESIGNATED PROPERLY - RE-FACTORED" * 5
379
380        # Check warning for time units pattern
381        if axis.isTime() and (not hasattr(axis, "units") or not time_units_pattn.match(axis.units)):
382            if self.time_units == None:
383                time_units_input = "I WON'T MATCH"
384
385                while time_units_input != "" and not time_units_pattn.match(time_units_input):
386                    message = time_units_warning_message                           
387                    if self.time_warning == True:
388                        print message
389                        time_units_input = raw_input("Please insert your time unit string here (or leave blank):").strip()
390                    else: 
391                        time_units_input = ""
392
393                self.output_message.append(message)
394                self.time_units = time_units_input
395
396            axis.units = self.time_units
397            axis.long_name = axis.name = "time (%s)" % self.time_units
398
399        if not hasattr(axis, "units") or axis.units == None: 
400            if units:
401                axis.units = units     
402            else:
403                axis.units = "Not known"
404
405        return axis
Note: See TracBrowser for help on using the repository browser.