source: nappy/trunk/nappy/nc_interface/na_content_collector.py @ 3348

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/nappy/trunk/nappy/nc_interface/na_content_collector.py@3348
Revision 3348, 21.5 KB checked in by astephen, 13 years ago (diff)
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"""
6cdms_to_na.py
7=============
8
9Holds the class CDMSToNA that converts a set of CDMS variables and global attributes.
10
11"""
12
13# Imports from python standard library
14import sys
15import os
16import time
17import string
18import re
19
20# Import from nappy package
21from nappy.na_error import na_error
22import nappy.utils
23import nappy.utils.axis_utils
24import nappy.cdms_utils.var_utils
25import nappy.utils.common_utils
26import nappy.na_file.na_core
27
28nc_to_na_map = utils.getConfigDict()["nc_to_na_map"]
29
30# Import external packages (if available)
31if sys.platform.find("win") > -1:
32    raise na_error.NAPlatformError("Windows does not support CDMS. CDMS is required to convert to CDMS objects and NetCDF.")
33try:
34    import cdms, Numeric
35except:
36    raise Exception("Could not import third-party software. Nappy requires the CDMS and Numeric packages to be installed to convert to CDMS and NetCDF.")
37
38cdms.setAutoBounds("off") 
39
40class CdmsToNABuilder --> NAContentCollector: (naDict, var_ids, varBin)
41__init__ --> sets everything up and runs it move some to --> analyse()
42analyseVariables
43defineNAVars
44defineNAAuxVars
45getAxisDefinition
46defineNAGlobals
47defineNAComments
48defineGeneralHeader
49
50
51
52class NAContentCollector(NACore):
53    """
54    Class to build a NASA Ames File object from a set of
55    CDMS variables and global attributes (optional).
56    """
57   
58    def __init__(self, vars, global_attributes={}, cdms_file=None):
59        """
60        Sets up instance variables and calls appropriate methods to
61        generate sections of NASA Ames file object.
62        """
63        self.cdms_file = cdms_file
64        self.output_message = []
65        self.na_dict = {}
66        self.vars = vars
67        self.var_ids = None
68        self.globals = global_attributes       
69        self.rank_zero_vars = []
70        self.rank_zero_var_ids = []
71        (self.ordered_vars, aux_vars) = self.analyseVariables()
72        if self.ordered_vars == None:
73            self.var_bin = []
74        else:
75
76            self.var_ids = [[var.id for var in self.ordered_vars],[var.id for var in aux_vars], self.rank_zero_var_ids]
77       
78            self.na_dict["NLHEAD"] = "-999"
79       
80            #print [var.id for var in self.ordered_vars]
81            #print [var.rank() for var in self.ordered_vars]   
82            self.defineNAVars(self.ordered_vars)
83            self.defineNAAuxVars(aux_vars)
84            self.defineNAGlobals()
85            self.defineNAComments()
86            self.defineGeneralHeader()
87            # Quick fudge
88            if self.na_dict["FFI"] == 1001: self.na_dict["X"] = self.na_dict["X"][0]
89
90
91    def analyseVariables(self):
92        """
93        Method to examine the content of CDMS variables to return
94        a tuple of two lists containing variables and auxiliary variables
95        for the NASA Ames file object.
96        Variables not compatible with the first file are binned to be used next.
97        """
98        # Need to group the variables together in bins
99        self.var_bin = []
100        # Get largest ranked variable as the one we use as standard
101        highrank = -1
102        best_var = None
103        count = 0
104        for var in self.vars:
105            msg = "Analysing: %s" % var.id
106            print msg
107            self.output_message.append(msg)
108            count = count + 1
109
110            # get rank
111            rank = var.rank()
112
113            # Deal with singleton variables
114            if rank == 0: 
115                self.rank_zero_vars.append(var)
116                self.rank_zero_var_ids.append(var.id)
117                continue
118           
119            if rank > highrank:
120                highrank = rank
121                best_var = var
122                best_var_index = count
123            elif rank == highrank:
124                if len(var.flat) > len(best_var.flat):
125                    best_var = var
126                    best_var_index = count
127       
128        if len(self.rank_zero_vars) == len(self.vars):  return (None, None)
129        if not best_var: 
130            print "No variables produced"
131            return (None, None)
132
133        vars_for_na = [best_var]
134        aux_vars_for_na = []
135        shape = best_var.shape
136        ndims = len(shape)
137        self.na_dict["NIV"] = ndims
138
139        # Work out which File Format Index is appropriate
140        if ndims in (2,3,4):
141            self.na_dict["FFI"] = 10 + (ndims * 1000)
142        elif ndims > 4:
143            raise "Cannot write variables defined against greater than 4 axes in NASA Ames format."
144        else:
145            if len(aux_vars_for_na) > 0 or (self.na_dict.has_key("NAUXV") and self.na_dict["NAUXV"] > 0):
146                self.na_dict["FFI"] = 1010
147            else:
148                self.na_dict["FFI"] = 1001
149
150        axes = best_var.getAxisList()
151       
152        # Get other variable info
153        for var in self.vars[:best_var_index - 1] + self.vars[best_var_index:]:
154
155            if var.id in self.rank_zero_var_ids: continue
156            if len(var.shape) != ndims or var.shape != shape: 
157                # Could it be an auxiliary variable
158                if len(var.shape) != 1: 
159                    self.var_bin.append(var)
160                    continue
161                caxis = var.getAxis(0)
162                if nappy.cdms_utils.axis_utils.compareAxes(axes[0], caxis) == 0: 
163                    self.var_bin.append(var)
164                    continue
165                # I think it is an auxiliary variable
166                aux_vars_for_na.append(var) 
167                # Also put it in var bin because auxiliary vars might be useful
168                self.var_bin.append(var)
169            else:
170                caxes = var.getAxisList()
171                #print var.id, "here"
172                for i in range(ndims):           
173                    if nappy.cdms_utils.axis_utils.compareAxes(axes[i], caxes[i]) == 0:
174                        self.var_bin.append(var)
175                        continue
176                # OK, I think they are compatible
177                vars_for_na.append(var)
178               
179        # Re-order if they previously came from NASA Ames files (i.e. including
180        # the attribute 'nasa_ames_var_number')
181        ordered_vars = [None] * 1000
182        other_vars = []
183        for var in vars_for_na:
184            if hasattr(var, "nasa_ames_var_number"):
185                ordered_vars[var.nasa_ames_var_number[0]] = var
186            else:
187                other_vars.append(var)
188        # Remake vars_for_na now in order
189        vars_for_na = []
190        for var in ordered_vars:
191            if var != None: vars_for_na.append(var)
192        vars_for_na = vars_for_na + other_vars
193
194        # Now re-order the Auxiliary variables if they previously came from NASA
195        # Ames files (i.e. including the attribute 'nasa_ames_aux_var_number')
196
197        ordered_aux_vars = [None] * 1000
198        other_aux_vars = []
199        for var in aux_vars_for_na:
200            if hasattr(var, "nasa_ames_aux_var_number"):
201                ordered_aux_vars[var.nasa_ames_aux_var_number[0]] = var
202            else:
203                other_aux_vars.append(var)
204        # Remake aux_vars_for_na now in order
205        aux_vars_for_na = []
206        for var in ordered_aux_vars:
207            if var != None: aux_vars_for_na.append(var)
208        aux_vars_for_na = aux_vars_for_na + other_aux_vars     
209        return (vars_for_na, aux_vars_for_na)
210
211
212    def defineNAVars(self, vars):
213        """
214        Method to define NASA Ames file object variables and their
215        associated metadata.
216        """
217        self.na_dict["NV"] = len(vars)
218        self.na_dict["VNAME"] = []
219        self.na_dict["VMISS"] = []
220        self.na_dict["VSCAL"] = []
221        self.na_dict["V"] = []
222        for var in vars:
223            name = nappy.cdms_utils.var_utils.getBestName(var)
224            self.na_dict["VNAME"].append(name)
225            miss = nappy.cdms_utils.var_utils.getMissingValue(var)
226            if type(miss) not in (float, int, long):  miss = miss[0]
227            self.na_dict["VMISS"].append(miss)
228            self.na_dict["VSCAL"].append(1)
229            # AND THE ARRAY
230            # Populate the variable list 
231            self.na_dict["V"].append(var._data)
232
233            if not self.na_dict.has_key("X"):
234                self.na_dict["NXDEF"] = []
235                self.na_dict["NX"] = []
236                # Create independent variable information
237                self.ax0 = var.getAxis(0)
238                self.na_dict["X"] = [list(self.ax0._data_)]
239                self.na_dict["XNAME"] = [nappy.cdms_utils.var_utils.getBestName(self.ax0)]
240                if len(self.ax0) == 1:
241                    self.na_dict["DX"] = [0]
242                else:
243                    incr = self.ax0[1] - self.ax0[0]
244                    # Set default increment as gap between first two
245                    self.na_dict["DX"] = [incr]
246                    # Now overwrite it as zero if non-uniform interval in axis
247                    for i in range(1, len(self.ax0)):
248                        if (self.ax0[i] - self.ax0[i - 1]) != incr:
249                            self.na_dict["DX"] = [0]
250                            break
251
252                # Now sort the rest of the axes
253                for axis in var.getAxisList()[1:]:
254                    self.getAxisDefinition(axis)
255
256
257    def defineNAAuxVars(self, auxVars):
258        """
259        Method to define NASA Ames file object auxiliary variables and their
260        associated metadata.
261        """
262        self.na_dict["NAUXV"] = len(auxVars)
263        self.na_dict["ANAME"] = []
264        self.na_dict["AMISS"] = []
265        self.na_dict["ASCAL"] = []
266        self.na_dict["A"] = []
267        for var in auxVars:
268            name = nappy.cdms_utils.var_utils.getBestName(var)
269            self.na_dict["ANAME"].append(name)
270            miss = nappy.cdms_utils.var_utils.getMissingValue(var)
271            if type(miss) != float:  miss = miss[0]
272            self.na_dict["AMISS"].append(miss)
273            self.na_dict["ASCAL"].append(1)
274            # AND THE ARRAY
275            # Populate the variable list 
276            self.na_dict["A"].append(var._data)
277
278    def getAxisDefinition(self, axis):
279        """
280        Method to create the appropriate NASA Ames file object
281        items associated with an axis (independent variable in
282        NASA Ames).
283        """
284        length = len(axis)
285        self.na_dict["NX"].append(length)
286        self.na_dict["XNAME"].append(nappy.cdms_utils.var_utils.getBestName(axis))
287        # If only one item in axis values
288        if length < 2:
289            self.na_dict["DX"].append(0)
290            self.na_dict["NXDEF"].append(length)
291            self.na_dict["X"].append(list(axis._data_))       
292            return
293   
294        incr = axis[1] - axis[0]
295        for i in range(1, length):
296            if (axis[i] - axis[i - 1]) != incr:
297                self.na_dict["DX"].append(0)
298                self.na_dict["NXDEF"].append(length)
299                self.na_dict["X"].append(list(axis._data_))
300                break
301        else:
302            max_length = length
303            if length > 3: max_length = 3
304            self.na_dict["DX"].append(incr)
305            self.na_dict["NXDEF"].append(max_length)
306            self.na_dict["X"].append(axis[:max_length])
307        return
308
309
310    def defineNAGlobals(self):
311        """
312        Maps CDMS (NetCDF) global attributes into NASA Ames Header fields.
313        """
314        # Check if we should add to it with locally set rules
315        locally_set_globals = localRules.localGlobalAttributes
316        for att in locally_set_globals.keys():
317            if not nc_to_na_map.has_key(att):
318                nc_to_na_map[key] = locally_set_globals[key]
319
320        self.extra_comments = [[],[],[]]  # Normal comments, special comments, other comments
321        convention_or_reference_comments = []
322
323        for key in self.globals.keys():
324            if key != "first_valid_date_of_data" and type(self.globals[key]) not in (str, float, int):
325                continue
326
327            if key in nc_to_na_map.keys():
328                if key == "history":
329                    timestring = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time()))
330                    history = "History:\t%s - Converted to NASA Ames format using nappy-%s.\n\t%s" % (timestring, version.version, self.globals[key])
331                    history = history.split("\n") 
332                    self.history = []
333                    for h in history:
334                        if h[:8] != "History:" and h[:1] != "\t": 
335                            h = "\t" + h
336                        self.history.append(h) 
337                   
338                elif key == "institution":
339                    # If fields came from NA then extract appropriate fields.
340                    match = re.match(r"(.*)\s+\(ONAME from NASA Ames file\);\s+(.*)\s+\(ORG from NASA Ames file\)\.", self.globals[key])
341                    if match:
342                        self.na_dict["ONAME"] = match.groups()[0]
343                        self.na_dict["ORG"] = match.groups()[1]
344                    else:
345                        self.na_dict["ONAME"] = self.globals[key]
346                        self.na_dict["ORG"] = self.globals[key]             
347                   
348                    # NOte: should probably do the following search and replace on all string lines
349                    self.na_dict["ONAME"] = self.na_dict["ONAME"].replace("\n", "  ")
350                    self.na_dict["ORG"] = self.na_dict["ORG"].replace("\n", "  ")
351                                   
352                elif key == "comment":
353                    # Need to work out if they are actually comments from NASA Ames in the first place
354                    #self.ncom = [self.globals[key]]
355                    comLines = self.globals[key].split("\n")
356                    normComms = []
357                    normCommFlag = None
358                    specComms = []
359                    specCommFlag = None
360                    for line in comLines:
361                        if line.find("###NASA Ames Special Comments follow###") > -1:
362                            specCommFlag = 1
363                        elif line.find("###NASA Ames Special Comments end###") > -1:
364                            specCommFlag = None
365                        elif line.find("###NASA Ames Normal Comments follow###") > -1:
366                            normCommFlag = 1
367                        elif line.find("###NASA Ames Normal Comments end###") > -1:
368                            normCommFlag = None 
369                        elif specCommFlag == 1:
370                            specComms.append(line)
371                        elif normCommFlag == 1:
372                            normComms.append(line)
373                        elif line.find("###Data Section begins on the next line###") > -1:
374                            pass
375                        else:
376                            normComms.append(line)         
377                   
378                    self.extra_comments = [specComms, normComms, []]               
379                                   
380                elif key == "first_valid_date_of_data":
381                    self.na_dict["DATE"] = self.globals[key]
382               
383                elif key in ("Conventions", "references"):
384                    #convention_or_reference_comments.append("%s:   %s" % (key, self.globals[key]))
385                    self.extra_comments[2].append("%s:   %s" % (key, self.globals[key]))
386                else:
387                    self.na_dict[nc_to_na_map[key]] = self.globals[key]
388            else:
389                self.extra_comments[2].append("%s:   %s" % (key, self.globals[key]))
390        #self.extra_comments
391        return
392
393
394    def defineNAComments(self, normal_comments=[], special_comments=[]):
395        """
396        Defines the Special and Normal comments sections in the NASA Ames file
397        object - including information gathered from the defineNAGlobals method.
398        """
399       
400        if hasattr(self, "ncom"):  normal_comments = self.ncom + normal_comments
401        NCOM = []
402        for ncom in normal_comments:
403            NCOM.append(ncom)
404        if len(NCOM) > 0:   NCOM.append("")
405       
406        if len(self.extra_comments[2]) > 0:
407            for excom in self.extra_comments[2]:
408                NCOM.append(excom)
409       
410        if len(self.extra_comments[1]) > 0: 
411            NCOM.append("Additional Global Attributes defined in the source file and not translated elsewhere:")
412            for excom in self.extra_comments[1]:
413                NCOM.append(excom)
414
415        if hasattr(self, "history"):
416            for h in self.history:
417                NCOM.append(h)
418       
419        if len(NCOM) > 0:
420            NCOM.insert(0, "###NASA Ames Normal Comments follow###")
421            NCOM.append("")
422            NCOM.append("###NASA Ames Normal Comments end###")
423        NCOM.append("###Data Section begins on the next line###")
424
425        specCommentsFlag = None
426        SCOM = []
427        special_comments = self.extra_comments[0]
428        if len(special_comments) > 0: 
429            SCOM = ["###NASA Ames Special Comments follow###"]
430            specCommentsFlag = 1
431        for scom in special_comments:
432            SCOM.append(scom)
433
434
435        #used_var_atts=("name", "long_name", "standard_name", "id",
436            #    "missing_value", "fill_value", "units",
437                #"nasa_ames_var_number", "nasa_ames_aux_var_number")
438        used_var_atts = ("id",  "missing_value", "fill_value", "units", 
439                   "nasa_ames_var_number", "nasa_ames_aux_var_number")
440        varCommentsFlag = None
441
442        # Create a string for the Special comments to hold rank-zero vars
443        rank_zero_varsString = []
444        for var in self.rank_zero_vars:
445            rank_zero_varsString.append("\tVariable %s: %s" % (var.id, nappy.cdms_utils.var_utils.getBestName(var)))
446            for att in var.attributes.keys():
447                value = var.attributes[att]
448                if type(value) in (str, float, int):
449                    rank_zero_varsString.append("\t\t%s = %s" % (att, var.attributes[att]))
450            #print "VALUES", dir(var), var._data ; rank_zero_varsString.append("\t\tvalue  =  %s" % var._data)
451       
452        if len(rank_zero_varsString) > 0:
453            rank_zero_varsString.insert(0, "###Singleton Variables defined in the source file follow###")
454            rank_zero_varsString.append("###Singleton Variables defined in the source file end###")
455
456        for var in self.ordered_vars:
457            varflag = "unused"
458            name = nappy.cdms_utils.var_utils.getBestName(var)
459            for scom,value in var.attributes.items():
460                if type(value) in (type([]), type(Numeric.array([0]))) and len(value) == 1:
461                    value = value[0]
462                if type(value) in (str, float, int) and scom not in used_var_atts:
463                    if varflag == "unused":
464                        if varCommentsFlag == None:
465                            varCommentsFlag = 1
466                            if specCommentsFlag == None:
467                                SCOM = ["###NASA Ames Special Comments follow###"] + rank_zero_varsString
468                            SCOM.append("Additional Variable Attributes defined in the source file and not translated elsewhere:")
469                            SCOM.append("###Variable attributes from source (NetCDF) file follow###")
470                        varflag = "using" 
471                        SCOM.append("\tVariable %s: %s" % (var.id, name))
472                    SCOM.append("\t\t%s = %s" % (scom, value))
473
474        if varCommentsFlag == 1:  SCOM.append("###Variable attributes from source (NetCDF) file end###")
475        if specCommentsFlag == 1:
476            SCOM.append("###NASA Ames Special Comments end###")
477
478        """used_var_atts = ("name", "long_name", "standard_name", "id", "missing_value", "fill_value", "units")
479        for var in self.vars:
480            for scom,value in var.attributes.items():
481                name = nappy.cdms_utils.var_utils.getBestName(var)
482                if type(value) in (str, float, int) and scom not in used_var_atts:
483                    SCOM.append("\t%s: %s - %s" % (name, scom, value))"""
484
485        # Strip out empty lines (or returns)
486        NCOM_cleaned = []
487        SCOM_cleaned = []
488        #hiddenNewLineCount1 = 0
489        for c in NCOM:
490            if c.strip() not in ("", " ", "  "):
491                #hiddenNewLineCount1 = hiddenNewLineCount1 + c.count("\n")
492                # Replace new lines within one attribute with a newline and tab so easier to read
493                lines = c.split("\n")
494                for line in lines:
495                    if line != lines[0]: line = "\t" + line
496                    NCOM_cleaned.append(line)
497               
498        #hiddenNewLineCount2 = 0       
499        for c in SCOM:
500            if c.strip() not in ("", " ", "  "):               
501                #hiddenNewLineCount2 = hiddenNewLineCount2 + c.count("\n")
502                # Replace new lines within one attribute with a newline and tab so easier to read
503                #c = c.replace("\n", "\n\t")
504                #SCOM_cleaned.append(c)
505                lines = c.split("\n")
506                for line in lines:
507                    if line != lines[0]: line = "\t" + line
508                    SCOM_cleaned.append(line)
509                   
510        self.na_dict["NCOM"] = NCOM_cleaned
511        self.na_dict["NNCOML"] = len(self.na_dict["NCOM"])# + hiddenNewLineCount1
512        self.na_dict["SCOM"] = SCOM_cleaned
513        self.na_dict["NSCOML"] = len(self.na_dict["SCOM"])# + hiddenNewLineCount2
514        return
515
516
517    def defineGeneralHeader(self, header_items={}):
518        """
519        Defines known header items and overwrites any with header_items
520        key/value pairs.
521        """
522        # Check if DATE field previously known in NASA Ames file
523        time_now = time.strftime("%Y %m %d", time.localtime(time.time())).split()
524        if not self.na_dict.has_key("RDATE"):
525            self.na_dict["RDATE"] = time_now
526       
527        if self.ax0.isTime():
528            # Get first date in list
529            try:
530                (unit, start_date) = re.match("(\w+)\s+?since\s+?(\d+-\d+-\d+)", self.ax0.units).groups()           
531                comptime = cdtime.s2c(start_date)
532                first_day = comptime.add(self.na_dict["X"][0][0], getattr(cdtime, unit.capitalize()))
533                self.na_dict["DATE"] = string.replace(str(first_day).split(" ")[0], "-", " ").split()
534            except:
535                msg = "Nappy Warning: Could not get the first date in the file. You will need to manually edit the output file."
536                print msg
537                self.output_message.append(msg)
538                self.na_dict["DATE"] = ("DATE", "NOT", "KNOWN")
539        else: 
540            if not self.na_dict.has_key("DATE"):
541                msg = "Nappy Warning: Could not get the first date in the file. You will need to manually edit the output file."
542                print msg
543                self.output_message.append(msg)
544                self.na_dict["DATE"] = ("DATE", "NOT", "KNOWN")
545        self.na_dict["IVOL"] = 1
546        self.na_dict["NVOL"] = 1
547        for key in header_items.keys():
548             self.na_dict[key] = header_items[key]
549        return
550
551
Note: See TracBrowser for help on using the repository browser.