Skip to content
Snippets Groups Projects
load.py 16.78 KiB
#!/usr/bin/env python
# encoding: utf-8
"""
This module handles loading data for some of the top level comparison
routines.

Created by evas Dec 2012.
Copyright (c) 2012 University of Wisconsin SSEC. All rights reserved.
"""

import logging
#from pycdf import CDFError
import numpy

import glance.delta    as delta
import glance.data     as dataobj
import glance.io       as io
import glance.graphics as maps
from glance.util        import get_percentage_from_mask
from glance.lonlat_util import check_lon_lat_equality, compare_spatial_invalidity
from glance.constants   import *

LOG = logging.getLogger(__name__)

def _get_and_analyze_lon_lat (fileObject,
                              latitudeVariableName, longitudeVariableName,
                              latitudeDataFilterFn=None, longitudeDataFilterFn=None,
                              alternateFilePath=None, fileDescriptior="") :
    """
    get the longitude and latitude data from the given file, assuming they are in the given variable names
    and analyze them to identify spacially invalid data (ie. data that would fall off the earth)
    
    This may result in a ValueError if the variable cannot be loaded.
    """
    
    # for the a file, do we have an alternate?
    fileToUse = fileObject
    if alternateFilePath is not None :
        LOG.info("Loading alternate file (" + alternateFilePath
                 + ") for file " + fileDescriptior + " longitude/latitude.")
        fileToUse = dataobj.FileInfo(alternateFilePath)
    
    # get the longitude
    LOG.info ('longitude name: ' + longitudeVariableName)
    lonObject = load_data_object (fileToUse, longitudeVariableName,
                                  rangeMin=-180,
                                  rangeMax=360,
                                  forceDType=numpy.float,
                                  dataFilter=longitudeDataFilterFn)
    
    # get the latitude
    LOG.info ('latitude name: '  + latitudeVariableName)
    latObject = load_data_object (fileToUse, latitudeVariableName,
                                  rangeMin=-90,
                                  rangeMax=90,
                                  forceDType=numpy.float,
                                  dataFilter=latitudeDataFilterFn)
    
    # we are going to have issues with our comparision if they aren't the same shape
    LOG.debug('latitude  shape: ' + str(latObject.data.shape))
    LOG.debug('longitude shape: ' + str(lonObject.data.shape))
    assert (latObject.data.shape == lonObject.data.shape)
    
    # build a mask of our spacially invalid data
    spaciallyInvalidMask = latObject.masks.ignore_mask | lonObject.masks.ignore_mask
    
    # analyze our spacially invalid data
    percentageOfSpaciallyInvalidPts, numberOfSpaciallyInvalidPts = get_percentage_from_mask(spaciallyInvalidMask)
    
    spatialStatInfo = {
                       TOTAL_NUM_INVALID_PTS_KEY: numberOfSpaciallyInvalidPts,
                       PERCENT_INVALID_PTS_KEY:    percentageOfSpaciallyInvalidPts
                       }
    
    return lonObject, latObject, spatialStatInfo

def handle_lon_lat_info (lon_lat_settings, a_file_object, b_file_object, output_path,
                         should_make_images=False, should_check_equality=True,
                         fullDPI=None, thumbDPI=None) :
    """
    Manage loading and comparing longitude and latitude information for two files
    
    This may result in a ValueError if the longitude or latitude cannot be loaded.
    This may result in a VariableComparisonError if the longitude or latitude cannot be compared due to size.
    
    """
    # a place to save some general stats about our lon/lat data
    spatialInfo = { }
    
    # if there is no lon/lat specified, stop now
    if ( (LONGITUDE_NAME_KEY not in lon_lat_settings) or (LATITUDE_NAME_KEY not in lon_lat_settings)
        or ((USE_NO_LON_OR_LAT_VARS_KEY in lon_lat_settings) and lon_lat_settings[USE_NO_LON_OR_LAT_VARS_KEY]) ) :
        return { }, { }
    
    # if we should not be comparing against the logitude and latitude, stop now
    LOG.debug ('lon_lat_settings: ' + str(lon_lat_settings))
    
    # figure out the names to be used for the longitude and latitude variables
    a_longitude_name = lon_lat_settings[LONGITUDE_NAME_KEY]
    a_latitude_name  = lon_lat_settings[LATITUDE_NAME_KEY]
    b_longitude_name = lon_lat_settings[LON_ALT_NAME_IN_B_KEY] if LON_ALT_NAME_IN_B_KEY in lon_lat_settings else a_longitude_name
    b_latitude_name  = lon_lat_settings[LAT_ALT_NAME_IN_B_KEY] if LAT_ALT_NAME_IN_B_KEY in lon_lat_settings else a_latitude_name
    
    # if we need to load our lon/lat from different files, open those files
    longitude_a_object, latitude_a_object, spatialInfo[A_FILE_TITLE_KEY] = \
                          _get_and_analyze_lon_lat (a_file_object,
                                                    a_latitude_name, a_longitude_name,
                              latitudeDataFilterFn=lon_lat_settings[LAT_FILTER_FUNCTION_A_KEY],
                              longitudeDataFilterFn=lon_lat_settings[LON_FILTER_FUNCTION_A_KEY],
                                                    alternateFilePath=lon_lat_settings[LONLAT_ALT_FILE_A_KEY] if (LONLAT_ALT_FILE_A_KEY in lon_lat_settings) else None,
                                                    fileDescriptior="a")
    longitude_b_object, latitude_b_object, spatialInfo[B_FILE_TITLE_KEY] = \
                          _get_and_analyze_lon_lat (b_file_object,
                                                    b_latitude_name, b_longitude_name,
                                                    latitudeDataFilterFn=lon_lat_settings[LAT_FILTER_FUNCTION_B_KEY],
                                                    longitudeDataFilterFn=lon_lat_settings[LON_FILTER_FUNCTION_B_KEY],
                                                    alternateFilePath=lon_lat_settings[LONLAT_ALT_FILE_B_KEY] if (LONLAT_ALT_FILE_B_KEY in lon_lat_settings) else None,
                                                    fileDescriptior="b")

    # make sure our lon and lat are cleaned up
    maps.clean_lon_and_lat(longitude_a_object, latitude_a_object, )
    maps.clean_lon_and_lat(longitude_b_object, latitude_b_object, )

    # if we need to, test the level of equality of the "valid" values in our lon/lat
    if should_check_equality :
        
        moreSpatialInfo = check_lon_lat_equality(longitude_a_object, latitude_a_object,
                                                 longitude_b_object, latitude_b_object,
                                                 lon_lat_settings[LON_LAT_EPSILON_KEY],
                                                 should_make_images, output_path,
                                                 fullDPI=fullDPI, thumbDPI=thumbDPI)
        # update our existing spatial information
        spatialInfo.update(moreSpatialInfo)
        
        # compare our spatially invalid info to see if the two files have invalid longitudes and latitudes in the same places
        spaciallyInvalidMask, spatialInfo, longitude_common, latitude_common = \
                                compare_spatial_invalidity(longitude_a_object, longitude_b_object,
                                                           latitude_a_object,  latitude_b_object,
                                                           spatialInfo, should_make_images, output_path,
                                                           fullDPI=fullDPI, thumbDPI=thumbDPI)
    else:
        spaciallyInvalidMask = None
        longitude_common     = None
        latitude_common      = None
    
    # FUTURE, return the lon/lat objects instead?
    return {
            A_FILE_KEY:
                      {
                       LON_KEY:             longitude_a_object.data,
                       LAT_KEY:             latitude_a_object.data,
                       INVALID_MASK_KEY:    longitude_a_object.masks.ignore_mask,
                       LON_FILL_VALUE_KEY:  longitude_a_object.fill_value,
                       LAT_FILL_VALUE_KEY:  latitude_a_object.fill_value
                      },
            B_FILE_KEY:
                      {
                       LON_KEY:             longitude_b_object.data,
                       LAT_KEY:             latitude_b_object.data,
                       INVALID_MASK_KEY:    longitude_b_object.masks.ignore_mask,
                       LON_FILL_VALUE_KEY:  longitude_b_object.fill_value,
                       LAT_FILL_VALUE_KEY:  latitude_b_object.fill_value
                      },
            COMMON_KEY:
                      {
                       LON_KEY:             longitude_common,
                       LAT_KEY:             latitude_common,
                       INVALID_MASK_KEY:    spaciallyInvalidMask
                      }
            }, \
           spatialInfo

def handle_lon_lat_info_for_one_file (lon_lat_settings, file_object) :
    """
    Manage loading longitude and latitude information for a file
    
    This may result in a ValueError if the longitude or latitude cannot be loaded.
    
    """
    
    # if there is no lon/lat specified, stop now
    if ( (LONGITUDE_NAME_KEY not in lon_lat_settings) or (LATITUDE_NAME_KEY not in lon_lat_settings)
        or ((USE_NO_LON_OR_LAT_VARS_KEY in lon_lat_settings) and lon_lat_settings[USE_NO_LON_OR_LAT_VARS_KEY]) ) :
        return { }, { }
    
    # print our settings for debugging purposes
    LOG.debug ('lon_lat_settings: ' + str(lon_lat_settings))
    
    # figure out the names to be used for the longitude and latitude variables
    lon_name = lon_lat_settings[LONGITUDE_NAME_KEY]
    lat_name = lon_lat_settings[LATITUDE_NAME_KEY ]
    
    # load our lon/lat data
    lon_object, lat_object, spatialInfo = \
                        _get_and_analyze_lon_lat (file_object,
                                                  lat_name, lon_name,
                                                  latitudeDataFilterFn=lon_lat_settings[LAT_FILTER_FUNCTION_A_KEY],
                                                  longitudeDataFilterFn=lon_lat_settings[LON_FILTER_FUNCTION_A_KEY],
                                                  alternateFilePath=lon_lat_settings[LONLAT_ALT_FILE_A_KEY] if (LONLAT_ALT_FILE_A_KEY in lon_lat_settings) else None)

    # make sure our lon and lat are cleaned up
    maps.clean_lon_and_lat(lon_object, lat_object, )

    # FUTURE, return the lon/lat objects instead?
    return {
            LON_KEY:             lon_object.data,
            LAT_KEY:             lat_object.data,
            INVALID_MASK_KEY:    lon_object.masks.ignore_mask | lat_object.masks.ignore_mask,
            LON_FILL_VALUE_KEY:  lon_object.fill_value,
            LAT_FILL_VALUE_KEY:  lat_object.fill_value
            }, \
           spatialInfo

def open_and_process_files (fileNames):
    """
    open files listed in the args and get information about the variables in them
    """
    # open all the files & get their variable names
    files = {}
    commonNames = None
    for fileName in fileNames:
        LOG.info("opening %s" % fileName)
        files[fileName] = {}
        tempFileObject = (io.open(fileName))
        files[fileName][FILE_OBJECT_KEY] = tempFileObject
        tempNames = set(tempFileObject())
        LOG.debug ('variable names for ' + fileName + ': ' + str(tempNames)) 
        files[fileName][FILE_VARIABLE_NAMES_KEY] = tempNames
        commonNames = tempNames if commonNames is None else commonNames.intersection(tempNames)
    files[COMMON_VAR_NAMES_KEY] = commonNames
    
    return files

class ValueErrorStringToFloat(Exception):
    pass

def load_variable_data(fileObject, variableNameInFile,
                       forceDType=None,
                       dataFilter=None,
                       variableToFilterOn=None,
                       variableBasedFilter=None,
                       altVariableFileObject=None,
                       fileDescriptionForDisplay="file",
                       correctForAWIPS=False) :
    """
    load data for a variable from a file
    optionally filter the variable data based on a data filter or another variable
    
    dataFilter must be in the form of (lambda data: some manipulation returning the new data)
    variableBasedFilter must be in the form of (lambda data, filterData: some manipulation returning the new data))
    """
    
    variableData     = None
    exceptionToRaise = None

    # get the data for the variable
    LOG.debug("loading basic data for variable " + variableNameInFile + " from " + fileDescriptionForDisplay)
    if fileObject is None :
        exceptionToRaise = ValueError("File was not properly opened so variable '" + variableNameInFile + "' could not be loaded.")
    else :
        try :
            variableData = numpy.array(fileObject[variableNameInFile]) if forceDType is None else numpy.array(fileObject[variableNameInFile], dtype=forceDType)
            variableData = variableData.astype(numpy.uint8) if correctForAWIPS else variableData
        except Exception as ex :
            if type(ex) is ValueError and str(ex) == "could not convert string to float: ":
                raise ValueErrorStringToFloat(str(ex))
            import traceback
            exceptionToRaise = ValueError('Unable to retrieve ' + variableNameInFile + ' data. The variable name' + 
                      ' may not exist in this file or an error may have occured while attempting to' +
                      ' access the data. Details of file access error observed: ' + str(ex))
    
    # if we ended up with an exception, raise that now
    if exceptionToRaise is not None :
        raise exceptionToRaise

    # apply the basic filter if there is one
    if dataFilter is not None :
        LOG.debug ("applying filter function to data from " + fileDescriptionForDisplay + " for variable " + variableNameInFile)
        variableData = dataFilter(variableData)
    
    # if we've got another variable to filter on, do that
    if (variableToFilterOn is not None) and (variableBasedFilter is not None) :
        LOG.debug ("filtering data from " + fileDescriptionForDisplay + " for variable " + variableNameInFile
                   + " based on additional data from variable " + variableToFilterOn)
        
        fileToUseTemp = fileObject
        if altVariableFileObject is not None :
            fileToUseTemp = altVariableFileObject # TODO, is this the right kind of object?
        
        dataToFilterOn = fileToUseTemp[variableToFilterOn]
        variableData   = variableBasedFilter(variableData, dataToFilterOn)
    
    return variableData

def load_data_object (fileObject, variableNameInFile,
                      rangeMin=None,
                      rangeMax=None,
                      forceDType=None,
                      dataFilter=None,
                      variableToFilterOn=None,
                      variableBasedFilter=None,
                      altVariableFileObject=None,
                      fileDescriptionForDisplay="file") :
    """
    load the data and put it into an appropriate DataObject
    
    Note: the rangeMin and rangeMax are the minimum and maximum acceptable data
    values used to calculate the invalid data mask
    """
    # get the data from the file
    rawData = load_variable_data(fileObject.file_object,
                                 variableNameInFile,
                                 forceDType=forceDType,
                                 dataFilter=dataFilter,
                                 variableToFilterOn=variableToFilterOn,
                                 variableBasedFilter=variableBasedFilter,
                                 altVariableFileObject=altVariableFileObject,
                                 fileDescriptionForDisplay=fileDescriptionForDisplay)
    
    # get the fill value
    fillValue = fileObject.file_object.missing_value(variableNameInFile)
    
    # build a mask of invalid data
    invalidMask = ~numpy.isfinite(rawData)
    if rangeMin  is not None :
        invalidMask |= rawData < rangeMin
    if rangeMax  is not None :
        invalidMask |= rawData > rangeMax
    if fillValue is not None :
        invalidMask |= rawData == fillValue
    
    return dataobj.DataObject(rawData, fillValue=fillValue, ignoreMask=invalidMask)

def get_UV_info_from_magnitude_direction_info(fileObject, magnitudeName, directionName, invalidMask=None) :
    """
    If there are magnitude and direction names, load that information and calculate the u and v that correspond to it
    """
    
    # if we don't have magnitude and direction, we can't calculate the U and V values
    if (magnitudeName is None) or (directionName is None) :
        return None, None
    
    # load the magnitude and direction data sets
    magnitude = load_variable_data(fileObject, magnitudeName)
    direction = load_variable_data(fileObject, directionName)
    
    # convert the magnitude and direction data into u and v vectors
    uData, vData = delta.convert_mag_dir_to_U_V_vector(magnitude, direction, invalidMask=invalidMask)
    
    return uData, vData

if __name__=='__main__':
    pass