From 79b4c3f9f138af886163cb737a36fdf280ecd61d Mon Sep 17 00:00:00 2001 From: sudhamurthy Date: Mon, 9 Sep 2024 10:24:23 -0400 Subject: [PATCH 01/41] DAS-2232 Initial commit for SMAP L3 spatial subset changes --- hoss/dimension_utilities.py | 193 ++++++++++++++++++++++++++++++----- hoss/hoss_config.json | 138 ++++++++++++++++++++----- hoss/projection_utilities.py | 33 ++++-- hoss/spatial.py | 117 +++++++++++++++++++-- hoss/subset.py | 24 +++-- pip_requirements.txt | 2 +- 6 files changed, 435 insertions(+), 72 deletions(-) diff --git a/hoss/dimension_utilities.py b/hoss/dimension_utilities.py index 9ee8d02..8a49622 100644 --- a/hoss/dimension_utilities.py +++ b/hoss/dimension_utilities.py @@ -20,15 +20,23 @@ from harmony.util import Config from netCDF4 import Dataset from numpy.ma.core import MaskedArray +from numpy import ndarray from varinfo import VariableFromDmr, VarInfoFromDmr from hoss.bbox_utilities import flatten_list from hoss.exceptions import InvalidNamedDimension, InvalidRequestedRange from hoss.utilities import ( format_variable_set_string, + format_dictionary_string, get_opendap_nc4, get_value_or_default, ) +from hoss.projection_utilities import ( + get_projected_x_y_extents, + get_projected_x_y_variables, + get_variable_crs, + get_x_y_extents_from_geographic_points +) IndexRange = Tuple[int] IndexRanges = Dict[str, IndexRange] @@ -63,7 +71,7 @@ def prefetch_dimension_variables( logger: Logger, access_token: str, config: Config, -) -> str: +) -> tuple[str, set[str]]: """Determine the dimensions that need to be "pre-fetched" from OPeNDAP in order to derive index ranges upon them. Initially, this was just spatial and temporal dimensions, but to support generic dimension @@ -73,21 +81,13 @@ def prefetch_dimension_variables( """ required_dimensions = varinfo.get_required_dimensions(required_variables) - - # Iterate through all requested dimensions and extract a list of bounds - # references for each that has any. This will produce a list of lists, - # which should be flattened into a single list and then combined into a set - # to remove duplicates. - bounds = set( - flatten_list( - [ - list(varinfo.get_variable(dimension).references.get('bounds')) - for dimension in required_dimensions - if varinfo.get_variable(dimension).references.get('bounds') is not None - ] - ) - ) - + if(len(required_dimensions) == 0): + #check if coordinate variables are provided + required_dimensions = varinfo.get_references_for_attribute(required_variables, 'coordinates') + logger.info('coordinates: ' f'{required_dimensions}') + + bounds = varinfo.get_references_for_attribute(required_dimensions, 'bounds') + logger.info('bounds: ' f'{bounds}') required_dimensions.update(bounds) logger.info( @@ -101,10 +101,139 @@ def prefetch_dimension_variables( # Create bounds variables if necessary. add_bounds_variables(required_dimensions_nc4, required_dimensions, varinfo, logger) + return required_dimensions_nc4, required_dimensions + + +def is_variable_one_dimensional( + prefetch_dataset: Dataset, dimension_variable: VariableFromDmr +) -> bool: + """Check if a dimension variable is 1D + + """ + dimensions_array = prefetch_dataset[dimension_variable.full_name_path][:] + return dimensions_array.ndim == 1 + + +def update_dimension_variables( + prefetch_dataset: Dataset, + required_dimensions: Set[str], + varinfo: VarInfoFromDmr, + logger: Logger, +) -> Dict[str, ndarray]: + """Augment a NetCDF4 file with artificial 1D dimensions variable for each + 2D dimension variable" + + For each dimension variable: + (1) Check if the dimension variable is 1D. + (2) If it is not 1D and is 2D create a dimensions array from + within the `write_1D_dimensions` + function. + (3) Then write the 1D dimensions variable to the NetCDF4 URL. - return required_dimensions_nc4 + """ + for dimension_name in required_dimensions: + dimension_variable = varinfo.get_variable(dimension_name) + logger.info('dimension name: ' f'{dimension_name}') + logger.info('dimension path:' f'{dimension_variable.full_name_path}') + + if is_variable_one_dimensional(prefetch_dataset, dimension_variable): + logger.info( + 'No changes needed: 'f'{dimension_name}' + ) + else: + col_size = prefetch_dataset[dimension_variable.full_name_path][:].shape[0] + row_size = prefetch_dataset[dimension_variable.full_name_path][:].shape[1] + crs = get_variable_crs(dimension_name, varinfo,logger) + logger.info('row_size=' f'{row_size}' ',col_size=' f'{col_size}') + + geo_grid_corners = get_geo_grid_corners( + prefetch_dataset, + required_dimensions, + varinfo, + logger + ) + x_y_extents = get_x_y_extents_from_geographic_points( + geo_grid_corners, crs + ) + + #get grid size and resolution + x_min = x_y_extents['x_min'] + x_max = x_y_extents['x_max'] + y_min = x_y_extents['y_min'] + y_max = x_y_extents['y_max'] + x_resolution = (x_max-x_min)/row_size + y_resolution = (y_max-y_min)/col_size + + logger.info('x_min:' f'{x_min}' ',x_max=' f'{x_max}' + ',y_min:' f'{y_min}' ',y_max=' f'{y_max}' + ',x_res=' f'{x_resolution}' 'y_res=' f'{y_resolution}') + + #create the xy dim scales + x_dim = np.arange(x_min, x_max, x_resolution) + + #ascending versus descending..should be based on the coordinate grid + y_dim = np.arange(y_max,y_min, -y_resolution) + logger.info('x_dim:' f'{x_dim}') + logger.info('y_dim:' f'{y_dim}') + return {'projected_y': y_dim, 'projected_x': x_dim} + +# to calculate grid corners when there are fill values +def get_geo_grid_corners( + prefetch_dataset: Dataset, + required_dimensions: Set[str], + varinfo: VarInfoFromDmr, + logger: Logger, +) -> list[Tuple[float]]: + """ + This method is used to return the lat lon corners from a 2D + coordinate dataset + """ + for dimension_name in required_dimensions: + dimension_variable = varinfo.get_variable(dimension_name) + if dimension_variable.is_latitude(): + lat_arr = prefetch_dataset[dimension_variable.full_name_path][:] + elif dimension_variable.is_longitude(): + lon_arr = prefetch_dataset[dimension_variable.full_name_path][:] + + #skip fill values when calculating min values + #topleft = minlon, maxlat + #bottomright = maxlon, minlat + top_left_row_idx = 0 + top_left_col_idx = 0 + lon_row = lon_arr[top_left_row_idx,:] + lon_row_valid_indices = np.where(lon_row >= -180.0)[0] + top_left_col_idx = lon_row_valid_indices[lon_row[lon_row_valid_indices].argmin()] + minlon = lon_row[top_left_col_idx] + top_right_col_idx = lon_row_valid_indices[lon_row[lon_row_valid_indices].argmax()] + maxlon = lon_row[top_right_col_idx] + + lat_col = lat_arr[:,top_right_col_idx] + lat_col_valid_indices = np.where(lat_col >= -180.0)[0] + bottom_right_row_idx = lat_col_valid_indices[lat_col[lat_col_valid_indices].argmin()] + minlat = lat_col[bottom_right_row_idx] + top_right_row_idx = lat_col_valid_indices[lat_col[lat_col_valid_indices].argmax()] + maxlat = lat_col[top_right_row_idx] + + topleft_corner = [minlon, maxlat] + topright_corner = [maxlon, maxlat] + bottomright_corner = [maxlon, minlat] + bottomleft_corner = [minlon, minlat] + + geo_grid_corners = [ + topleft_corner, + topright_corner, + bottomright_corner, + bottomleft_corner + ] + logger.info('topleft:' f'{topleft_corner}' + 'topright:' f'{topright_corner}' + ',bottomright:' f'{bottomright_corner}' + ',bottomleft:' f'{bottomleft_corner}') + + return geo_grid_corners + def add_bounds_variables( dimensions_nc4: str, required_dimensions: Set[str], @@ -409,7 +538,8 @@ def get_dimension_indices_from_bounds( def add_index_range( - variable_name: str, varinfo: VarInfoFromDmr, index_ranges: IndexRanges + variable_name: str, varinfo: VarInfoFromDmr, index_ranges: IndexRanges, + logger:Logger ) -> str: """Append the index ranges of each dimension for the specified variable. If there are no dimensions with listed index ranges, then the full @@ -422,16 +552,25 @@ def add_index_range( """ variable = varinfo.get_variable(variable_name) - + logger.info('variable name:' f'{variable_name}') range_strings = [] - - for dimension in variable.dimensions: - dimension_range = index_ranges.get(dimension) - - if dimension_range is not None and dimension_range[0] <= dimension_range[1]: - range_strings.append(f'[{dimension_range[0]}:{dimension_range[1]}]') - else: - range_strings.append('[]') + if(variable.dimensions == []): + for dimension in index_ranges.keys(): + dimension_range = index_ranges.get(dimension) + logger.info('dimension=' f'{dimension}' ', dimension_range=' f'{dimension_range}') + if dimension_range is not None and dimension_range[0] <= dimension_range[1]: + range_strings.append(f'[{dimension_range[0]}:{dimension_range[1]}]') + else: + range_strings.append('[]') + + else : + for dimension in variable.dimensions: + dimension_range = index_ranges.get(dimension) + + if dimension_range is not None and dimension_range[0] <= dimension_range[1]: + range_strings.append(f'[{dimension_range[0]}:{dimension_range[1]}]') + else: + range_strings.append('[]') if all(range_string == '[]' for range_string in range_strings): indices_string = '' diff --git a/hoss/hoss_config.json b/hoss/hoss_config.json index c214d6b..8f908ee 100644 --- a/hoss/hoss_config.json +++ b/hoss/hoss_config.json @@ -59,24 +59,6 @@ "Epoch": "2018-01-01T00:00:00.000000" } ], - "Grid_Mapping_Data": [ - { - "Grid_Mapping_Dataset_Name": "EASE2_Global", - "grid_mapping_name": "lambert_cylindrical_equal_area", - "standard_parallel": 30.0, - "longitude_of_central_meridian": 0.0, - "false_easting": 0.0, - "false_northing": 0.0 - }, - { - "Grid_Mapping_Dataset_Name": "EASE2_Polar", - "grid_mapping_name": "lambert_azimuthal_equal_area", - "longitude_of_projection_origin": 0.0, - "latitude_of_projection_origin": 90.0, - "false_easting": 0.0, - "false_northing": 0.0 - } - ], "CF_Overrides": [ { "Applicability": { @@ -170,8 +152,8 @@ }, "Attributes": [ { - "Name": "Grid_Mapping", - "Value": "EASE2_Global" + "Name": "grid_mapping", + "Value": "/EASE2_global_projection" } ], "_Description": "Some versions of these collections omit global grid mapping information" @@ -182,14 +164,46 @@ }, "Attributes": [ { - "Name": "Grid_Mapping", - "Value": "EASE2_Polar" + "Name": "grid_mapping", + "Value": "/EASE2_polar_projection" } ], "_Description": "Some versions of these collections omit polar grid mapping information" } ] }, + { + "Applicability": { + "Mission": "SMAP", + "ShortNamePath": "SPL3SMP_E" + }, + "Applicability_Group": [ + { + "Applicability": { + "Variable_Pattern": "Soil_Moisture_Retrieval_Data_(A|P)M/.*" + }, + "Attributes": [ + { + "Name": "grid_mapping", + "Value": "/EASE2_global_projection" + } + ], + "_Description": "Some versions of these collections omit global grid mapping information" + }, + { + "Applicability": { + "Variable_Pattern": "Soil_Moisture_Retrieval_Data_Polar_(A|P)M/.*" + }, + "Attributes": [ + { + "Name": "grid_mapping", + "Value": "/EASE2_polar_projection" + } + ], + "_Description": "Some versions of these collections omit polar grid mapping information" + } + ] + }, { "Applicability": { "Mission": "SMAP", @@ -197,12 +211,88 @@ }, "Attributes": [ { - "Name": "Grid_Mapping", - "Value": "EASE2_Polar" + "Name": "grid_mapping", + "Value": "/EASE2_polar_projection" } ], "_Description": "Some versions of these collections omit polar grid mapping information" }, + { + "Applicability": { + "Mission": "SMAP", + "ShortNamePath": "SPL3SM(P|A|AP)|SPL2SMAP_S" + }, + "Attributes": [ + { + "Name": "grid_mapping", + "Value": "/EASE2_global_projection" + } + ], + "_Description": "Some versions of these collections omit global grid mapping information" + }, + { + "Applicability": { + "Mission": "SMAP", + "ShortNamePath": "SPL3FT(P|P_E)|SPL3SM(P|P_E|A|AP)|SPL2SMAP_S" + }, + "Applicability_Group": [ + { + "Applicability": { + "Variable_Pattern": "/EASE2_global_projection" + }, + "Attributes": [ + { + "Name": "grid_mapping_name", + "Value": "lambert_cylindrical_equal_area" + }, + { + "Name":"standard_parallel", + "Value": 30.0 + }, + { + "Name": "longitude_of_central_meridian", + "Value": 0.0 + }, + { + "Name": "false_easting", + "Value": 0.0 + }, + { + "Name": "false_northing", + "Value": 0.0 + } + ], + "_Description": "Some versions of these collections omit global grid mapping information" + }, + { + "Applicability": { + "Variable_Pattern": "/EASE2_polar_projection" + }, + "Attributes": [ + { + "Name": "grid_mapping_name", + "Value": "lambert_azimuthal_equal_area" + }, + { "Name": "longitude_of_projection_origin", + "Value" : 0.0 + }, + { + "Name": "latitude_of_projection_origin", + "Value": 90.0 + }, + { + "Name": "false_easting", + "Value": 0.0 + }, + { + "Name": "false_northing", + "Value": 0.0 + } + ], + "_Description": "Some versions of these collections omit polar grid mapping information" + } + ] + }, { "Applicability": { "Mission": "SMAP", diff --git a/hoss/projection_utilities.py b/hoss/projection_utilities.py index bdacdc0..63997f9 100644 --- a/hoss/projection_utilities.py +++ b/hoss/projection_utilities.py @@ -12,7 +12,7 @@ import json from typing import Dict, List, Optional, Tuple, Union, get_args - +from logging import Logger import numpy as np from pyproj import CRS, Transformer from shapely.geometry import ( @@ -40,7 +40,7 @@ Shape = Union[LineString, Point, Polygon, MultiShape] -def get_variable_crs(variable: str, varinfo: VarInfoFromDmr) -> CRS: +def get_variable_crs(variable: str, varinfo: VarInfoFromDmr, logger: Logger) -> CRS: """Check the metadata attributes for the variable to find the associated grid mapping variable. Create a `pyproj.CRS` object from the grid mapping variable metadata attributes. @@ -57,12 +57,33 @@ def get_variable_crs(variable: str, varinfo: VarInfoFromDmr) -> CRS: if grid_mapping is not None: try: - crs = CRS.from_cf(varinfo.get_variable(grid_mapping).attributes) + logger.info('grid_mapping first try: ' f'{grid_mapping}') + grid_mapping_variable = varinfo.get_variable(grid_mapping) + if grid_mapping_variable is not None: + crs = CRS.from_cf(varinfo.get_variable(grid_mapping).attributes) + else: + #logger.info('grid_mapping missing variable for: ' f'{grid_mapping}') + cf = varinfo.get_missing_variable_attributes(grid_mapping) + #logger.info('cf attr for missing variable for: ' f'{grid_mapping}' f'{cf}') + crs = CRS.from_cf(cf) + logger.info('crs missing variable for: ' f'{grid_mapping}' f'{crs}') except AttributeError as exception: - raise MissingGridMappingVariable(grid_mapping, variable) from exception + #check if there is a CF override + crs = CRS.from_cf(varinfo.get_missing_variable_attributes(grid_mapping)) + if crs is None: + raise MissingGridMappingVariable(grid_mapping, variable) from exception + + #check if there is a CF override else: - raise MissingGridMappingMetadata(variable) - + variable1 = varinfo.get_variable(variable) + logger.info('variable-name:' f'{variable} ', 'variable:' f'{variable1.name}') + grid_mapping = variable1.attributes.get('grid_mapping') + logger.info('grid_mapping second try: ' f'{grid_mapping}') + cf = varinfo.get_missing_variable_attributes(grid_mapping) + logger.info('cf: ' f'{cf}') + crs = CRS.from_cf(cf) + if crs is None: + raise MissingGridMappingMetadata(variable) return crs diff --git a/hoss/spatial.py b/hoss/spatial.py index 79eb5c2..475756b 100644 --- a/hoss/spatial.py +++ b/hoss/spatial.py @@ -23,7 +23,7 @@ """ from typing import List, Set - +from logging import Logger from harmony.message import Message from netCDF4 import Dataset from numpy.ma.core import MaskedArray @@ -41,6 +41,7 @@ get_dimension_bounds, get_dimension_extents, get_dimension_index_range, + update_dimension_variables ) from hoss.projection_utilities import ( get_projected_x_y_extents, @@ -48,12 +49,15 @@ get_variable_crs, ) +from hoss.utilities import format_dictionary_string def get_spatial_index_ranges( required_variables: Set[str], + required_dimensions: Set[str], varinfo: VarInfoFromDmr, dimensions_path: str, harmony_message: Message, + logger: Logger, shape_file_path: str = None, ) -> IndexRanges: """Return a dictionary containing indices that correspond to the minimum @@ -89,7 +93,11 @@ def get_spatial_index_ranges( non_spatial_variables = required_variables.difference( varinfo.get_spatial_dimensions(required_variables) ) - + logger.info('bounding_box: ' f'{bounding_box}' + '\r\n geo dims:' f'{geographic_dimensions}' + '\r\n proj dims:' f'{projected_dimensions}' + '\r\n non dims:' f'{non_spatial_variables}') + with Dataset(dimensions_path, 'r') as dimensions_file: if len(geographic_dimensions) > 0: # If there is no bounding box, but there is a shape file, calculate @@ -102,8 +110,9 @@ def get_spatial_index_ranges( index_ranges[dimension] = get_geographic_index_range( dimension, varinfo, dimensions_file, bounding_box ) - - if len(projected_dimensions) > 0: + return index_ranges + + elif len(projected_dimensions) > 0: for non_spatial_variable in non_spatial_variables: index_ranges.update( get_projected_x_y_index_ranges( @@ -111,12 +120,29 @@ def get_spatial_index_ranges( varinfo, dimensions_file, index_ranges, + logger, bounding_box=bounding_box, shape_file_path=shape_file_path, ) ) - - return index_ranges + return index_ranges + + elif len(required_dimensions) > 0: + for non_spatial_variable in non_spatial_variables: + index_ranges.update( + get_required_x_y_index_ranges( + non_spatial_variable, + varinfo, + dimensions_file, + required_dimensions, + index_ranges, + logger, + bounding_box=bounding_box, + shape_file_path=shape_file_path, + ) + ) + logger.info('non_spatial_variable:' f'{non_spatial_variable}' ',index_ranges:' f'{format_dictionary_string(index_ranges)}') + return index_ranges def get_projected_x_y_index_ranges( @@ -124,6 +150,7 @@ def get_projected_x_y_index_ranges( varinfo: VarInfoFromDmr, dimensions_file: Dataset, index_ranges: IndexRanges, + logger: Logger, bounding_box: BBox = None, shape_file_path: str = None, ) -> IndexRanges: @@ -153,8 +180,9 @@ def get_projected_x_y_index_ranges( and projected_y is not None and not set((projected_x, projected_y)).issubset(set(index_ranges.keys())) ): - crs = get_variable_crs(non_spatial_variable, varinfo) - + crs = get_variable_crs(non_spatial_variable, varinfo,logger) + logger.info('crs=' f'{crs}') + x_y_extents = get_projected_x_y_extents( dimensions_file[projected_x][:], dimensions_file[projected_y][:], @@ -181,11 +209,84 @@ def get_projected_x_y_index_ranges( ) x_y_index_ranges = {projected_x: x_index_ranges, projected_y: y_index_ranges} + else: x_y_index_ranges = {} return x_y_index_ranges +def get_required_x_y_index_ranges( + non_spatial_variable: str, + varinfo: VarInfoFromDmr, + coordinates_file: Dataset, + required_dimensions: Set[str], + index_ranges: IndexRanges, + logger: Logger, + bounding_box: BBox = None, + shape_file_path: str = None, +) -> IndexRanges: + """This function returns a dictionary containing the minimum and maximum + index ranges for a pair of projection x and y coordinates, e.g.: + + index_ranges = {'/x': (20, 42), '/y': (31, 53)} + + First, the dimensions of the input, non-spatial variable are checked + for associated projection x and y coordinates. If these are present, + and they have not already been added to the `index_ranges` cache, the + extents of the input spatial subset are determined in these projected + coordinates. This requires the derivation of a minimum resolution of + the target grid in geographic coordinates. Points must be placed along + the exterior of the spatial subset shape. All points are then projected + from a geographic Coordinate Reference System (CRS) to the target grid + CRS. The minimum and maximum values are then derived from these + projected coordinate points. + + """ + projected_x = 'projected_x' + projected_y = 'projected_y' + dimensions_file = update_dimension_variables( + coordinates_file, + required_dimensions, + varinfo, + logger, + ) + crs = get_variable_crs(non_spatial_variable, varinfo,logger) + + x_y_extents = get_projected_x_y_extents( + dimensions_file[projected_x][:], + dimensions_file[projected_y][:], + #xdims, + #ydims, + crs, + shape_file=shape_file_path, + bounding_box=bounding_box, + ) + logger.info('x_y_extents:' f'{format_dictionary_string(x_y_extents)}') + + x_index_ranges = get_dimension_index_range( + dimensions_file[projected_x][:], + #xdims, + x_y_extents['x_min'], + x_y_extents['x_max'], + #bounds_values=x_bounds, + ) + logger.info('x_index_ranges: ' f'{x_index_ranges}') + + y_index_ranges = get_dimension_index_range( + dimensions_file[projected_y][:], + #ydims, + x_y_extents['y_min'], + x_y_extents['y_max'], + #bounds_values=y_bounds, + ) + logger.info('y_index_ranges: ' f'{y_index_ranges}') + + x_y_index_ranges = {projected_y: y_index_ranges, projected_x: x_index_ranges} + + logger.info('x_y_index_ranges-str:' f'{format_dictionary_string(x_y_index_ranges)}') + logger.info('x_y_index_ranges: ' f'{x_y_index_ranges}') + + return x_y_index_ranges def get_geographic_index_range( dimension: str, diff --git a/hoss/subset.py b/hoss/subset.py index 8a01321..c8043be 100644 --- a/hoss/subset.py +++ b/hoss/subset.py @@ -27,9 +27,11 @@ ) from hoss.spatial import get_spatial_index_ranges from hoss.temporal import get_temporal_index_ranges -from hoss.utilities import download_url, format_variable_set_string, get_opendap_nc4 - - +from hoss.utilities import (download_url, + format_variable_set_string, + get_opendap_nc4, + format_dictionary_string +) def subset_granule( opendap_url: str, harmony_source: Source, @@ -87,7 +89,7 @@ def subset_granule( if request_is_index_subset: # Prefetch all dimension variables in full: - dimensions_path = prefetch_dimension_variables( + dimensions_path, required_dimensions = prefetch_dimension_variables( opendap_url, varinfo, required_variables, @@ -122,16 +124,26 @@ def subset_granule( shape_file_path = get_request_shape_file( harmony_message, output_dir, logger, config ) + logger.info( + 'All required variables: ' + f'{format_variable_set_string(required_variables)}' + ', dimensions_path: ' f'{dimensions_path}' + ', shapefilepath:' f'{shape_file_path}' + ) + index_ranges.update( get_spatial_index_ranges( required_variables, + required_dimensions, varinfo, dimensions_path, harmony_message, + logger, shape_file_path, ) ) - + logger.info('subset_granule - index_ranges:' f'{format_dictionary_string(index_ranges)}') + if harmony_message.temporal is not None: # Update `index_ranges` cache with ranges for temporal # variables. This will convert information from the temporal range @@ -144,7 +156,7 @@ def subset_granule( # Add any range indices to variable names for DAP4 constraint expression. variables_with_ranges = set( - add_index_range(variable, varinfo, index_ranges) + add_index_range(variable, varinfo, index_ranges, logger) for variable in required_variables ) logger.info( diff --git a/pip_requirements.txt b/pip_requirements.txt index 41ba9c5..1bb9bce 100644 --- a/pip_requirements.txt +++ b/pip_requirements.txt @@ -1,6 +1,6 @@ # This file should contain requirements to be installed via Pip. # Open source packages available from PyPI -earthdata-varinfo ~= 1.0.0 +earthdata-varinfo ~= 2.3.0 harmony-service-lib ~= 1.0.25 netCDF4 ~= 1.6.4 numpy ~= 1.24.2 From 789cc6b3761fdfb38043894caf51ddcd2bbcae1a Mon Sep 17 00:00:00 2001 From: sudhamurthy Date: Mon, 9 Sep 2024 11:24:08 -0400 Subject: [PATCH 02/41] corrections from pre-commit checks --- hoss/dimension_utilities.py | 179 +++++++++++++++++++---------------- hoss/projection_utilities.py | 19 ++-- hoss/spatial.py | 89 +++++++++-------- hoss/subset.py | 29 +++--- 4 files changed, 177 insertions(+), 139 deletions(-) diff --git a/hoss/dimension_utilities.py b/hoss/dimension_utilities.py index 8a49622..3235dc8 100644 --- a/hoss/dimension_utilities.py +++ b/hoss/dimension_utilities.py @@ -19,23 +19,23 @@ from harmony.message_utility import rgetattr from harmony.util import Config from netCDF4 import Dataset -from numpy.ma.core import MaskedArray from numpy import ndarray +from numpy.ma.core import MaskedArray from varinfo import VariableFromDmr, VarInfoFromDmr from hoss.bbox_utilities import flatten_list from hoss.exceptions import InvalidNamedDimension, InvalidRequestedRange -from hoss.utilities import ( - format_variable_set_string, - format_dictionary_string, - get_opendap_nc4, - get_value_or_default, -) from hoss.projection_utilities import ( get_projected_x_y_extents, get_projected_x_y_variables, get_variable_crs, - get_x_y_extents_from_geographic_points + get_x_y_extents_from_geographic_points, +) +from hoss.utilities import ( + format_dictionary_string, + format_variable_set_string, + get_opendap_nc4, + get_value_or_default, ) IndexRange = Tuple[int] @@ -81,11 +81,13 @@ def prefetch_dimension_variables( """ required_dimensions = varinfo.get_required_dimensions(required_variables) - if(len(required_dimensions) == 0): - #check if coordinate variables are provided - required_dimensions = varinfo.get_references_for_attribute(required_variables, 'coordinates') + if len(required_dimensions) == 0: + # check if coordinate variables are provided + required_dimensions = varinfo.get_references_for_attribute( + required_variables, 'coordinates' + ) logger.info('coordinates: ' f'{required_dimensions}') - + bounds = varinfo.get_references_for_attribute(required_dimensions, 'bounds') logger.info('bounds: ' f'{bounds}') required_dimensions.update(bounds) @@ -105,11 +107,9 @@ def prefetch_dimension_variables( def is_variable_one_dimensional( - prefetch_dataset: Dataset, dimension_variable: VariableFromDmr + prefetch_dataset: Dataset, dimension_variable: VariableFromDmr ) -> bool: - """Check if a dimension variable is 1D - - """ + """Check if a dimension variable is 1D""" dimensions_array = prefetch_dataset[dimension_variable.full_name_path][:] return dimensions_array.ndim == 1 @@ -125,7 +125,7 @@ def update_dimension_variables( For each dimension variable: (1) Check if the dimension variable is 1D. - (2) If it is not 1D and is 2D create a dimensions array from + (2) If it is not 1D and is 2D create a dimensions array from within the `write_1D_dimensions` function. (3) Then write the 1D dimensions variable to the NetCDF4 URL. @@ -135,59 +135,63 @@ def update_dimension_variables( dimension_variable = varinfo.get_variable(dimension_name) logger.info('dimension name: ' f'{dimension_name}') logger.info('dimension path:' f'{dimension_variable.full_name_path}') - + if is_variable_one_dimensional(prefetch_dataset, dimension_variable): - logger.info( - 'No changes needed: 'f'{dimension_name}' - ) - else: + logger.info('No changes needed: ' f'{dimension_name}') + else: col_size = prefetch_dataset[dimension_variable.full_name_path][:].shape[0] row_size = prefetch_dataset[dimension_variable.full_name_path][:].shape[1] - crs = get_variable_crs(dimension_name, varinfo,logger) + crs = get_variable_crs(dimension_name, varinfo, logger) logger.info('row_size=' f'{row_size}' ',col_size=' f'{col_size}') - - geo_grid_corners = get_geo_grid_corners( - prefetch_dataset, - required_dimensions, - varinfo, - logger - ) - x_y_extents = get_x_y_extents_from_geographic_points( - geo_grid_corners, crs + geo_grid_corners = get_geo_grid_corners( + prefetch_dataset, required_dimensions, varinfo, logger ) - - #get grid size and resolution + + x_y_extents = get_x_y_extents_from_geographic_points(geo_grid_corners, crs) + + # get grid size and resolution x_min = x_y_extents['x_min'] x_max = x_y_extents['x_max'] y_min = x_y_extents['y_min'] y_max = x_y_extents['y_max'] - x_resolution = (x_max-x_min)/row_size - y_resolution = (y_max-y_min)/col_size - - logger.info('x_min:' f'{x_min}' ',x_max=' f'{x_max}' - ',y_min:' f'{y_min}' ',y_max=' f'{y_max}' - ',x_res=' f'{x_resolution}' 'y_res=' f'{y_resolution}') - - #create the xy dim scales + x_resolution = (x_max - x_min) / row_size + y_resolution = (y_max - y_min) / col_size + + logger.info( + 'x_min:' + f'{x_min}' + ',x_max=' + f'{x_max}' + ',y_min:' + f'{y_min}' + ',y_max=' + f'{y_max}' + ',x_res=' + f'{x_resolution}' + 'y_res=' + f'{y_resolution}' + ) + + # create the xy dim scales x_dim = np.arange(x_min, x_max, x_resolution) - - #ascending versus descending..should be based on the coordinate grid - y_dim = np.arange(y_max,y_min, -y_resolution) - logger.info('x_dim:' f'{x_dim}') - logger.info('y_dim:' f'{y_dim}') + + # ascending versus descending..should be based on the coordinate grid + y_dim = np.arange(y_max, y_min, -y_resolution) return {'projected_y': y_dim, 'projected_x': x_dim} - -# to calculate grid corners when there are fill values -def get_geo_grid_corners( + + +def get_geo_grid_corners( prefetch_dataset: Dataset, required_dimensions: Set[str], varinfo: VarInfoFromDmr, logger: Logger, ) -> list[Tuple[float]]: """ - This method is used to return the lat lon corners from a 2D - coordinate dataset + This method is used to return the lat lon corners from a 2D + coordinate dataset. This does a check for values below -180 + which could be fill values. Does not check if there are fill + values in the corner points to go down to the next row and col """ for dimension_name in required_dimensions: @@ -196,44 +200,53 @@ def get_geo_grid_corners( lat_arr = prefetch_dataset[dimension_variable.full_name_path][:] elif dimension_variable.is_longitude(): lon_arr = prefetch_dataset[dimension_variable.full_name_path][:] - - #skip fill values when calculating min values - #topleft = minlon, maxlat - #bottomright = maxlon, minlat - top_left_row_idx = 0 + + # skip fill values when calculating min values + # topleft = minlon, maxlat + # bottomright = maxlon, minlat + top_left_row_idx = 0 top_left_col_idx = 0 - lon_row = lon_arr[top_left_row_idx,:] + lon_row = lon_arr[top_left_row_idx, :] lon_row_valid_indices = np.where(lon_row >= -180.0)[0] top_left_col_idx = lon_row_valid_indices[lon_row[lon_row_valid_indices].argmin()] - minlon = lon_row[top_left_col_idx] + minlon = lon_row[top_left_col_idx] top_right_col_idx = lon_row_valid_indices[lon_row[lon_row_valid_indices].argmax()] maxlon = lon_row[top_right_col_idx] - - lat_col = lat_arr[:,top_right_col_idx] + + lat_col = lat_arr[:, top_right_col_idx] lat_col_valid_indices = np.where(lat_col >= -180.0)[0] - bottom_right_row_idx = lat_col_valid_indices[lat_col[lat_col_valid_indices].argmin()] + bottom_right_row_idx = lat_col_valid_indices[ + lat_col[lat_col_valid_indices].argmin() + ] minlat = lat_col[bottom_right_row_idx] top_right_row_idx = lat_col_valid_indices[lat_col[lat_col_valid_indices].argmax()] maxlat = lat_col[top_right_row_idx] - + topleft_corner = [minlon, maxlat] topright_corner = [maxlon, maxlat] bottomright_corner = [maxlon, minlat] bottomleft_corner = [minlon, minlat] - - geo_grid_corners = [ - topleft_corner, - topright_corner, - bottomright_corner, - bottomleft_corner - ] - logger.info('topleft:' f'{topleft_corner}' - 'topright:' f'{topright_corner}' - ',bottomright:' f'{bottomright_corner}' - ',bottomleft:' f'{bottomleft_corner}') + + geo_grid_corners = [ + topleft_corner, + topright_corner, + bottomright_corner, + bottomleft_corner, + ] + logger.info( + 'topleft:' + f'{topleft_corner}' + 'topright:' + f'{topright_corner}' + ',bottomright:' + f'{bottomright_corner}' + ',bottomleft:' + f'{bottomleft_corner}' + ) return geo_grid_corners - + + def add_bounds_variables( dimensions_nc4: str, required_dimensions: Set[str], @@ -538,8 +551,10 @@ def get_dimension_indices_from_bounds( def add_index_range( - variable_name: str, varinfo: VarInfoFromDmr, index_ranges: IndexRanges, - logger:Logger + variable_name: str, + varinfo: VarInfoFromDmr, + index_ranges: IndexRanges, + logger: Logger, ) -> str: """Append the index ranges of each dimension for the specified variable. If there are no dimensions with listed index ranges, then the full @@ -554,16 +569,18 @@ def add_index_range( variable = varinfo.get_variable(variable_name) logger.info('variable name:' f'{variable_name}') range_strings = [] - if(variable.dimensions == []): + if variable.dimensions == []: for dimension in index_ranges.keys(): dimension_range = index_ranges.get(dimension) - logger.info('dimension=' f'{dimension}' ', dimension_range=' f'{dimension_range}') + logger.info( + 'dimension=' f'{dimension}' ', dimension_range=' f'{dimension_range}' + ) if dimension_range is not None and dimension_range[0] <= dimension_range[1]: range_strings.append(f'[{dimension_range[0]}:{dimension_range[1]}]') else: range_strings.append('[]') - - else : + + else: for dimension in variable.dimensions: dimension_range = index_ranges.get(dimension) diff --git a/hoss/projection_utilities.py b/hoss/projection_utilities.py index 63997f9..7924ed6 100644 --- a/hoss/projection_utilities.py +++ b/hoss/projection_utilities.py @@ -11,8 +11,9 @@ """ import json -from typing import Dict, List, Optional, Tuple, Union, get_args from logging import Logger +from typing import Dict, List, Optional, Tuple, Union, get_args + import numpy as np from pyproj import CRS, Transformer from shapely.geometry import ( @@ -62,18 +63,18 @@ def get_variable_crs(variable: str, varinfo: VarInfoFromDmr, logger: Logger) -> if grid_mapping_variable is not None: crs = CRS.from_cf(varinfo.get_variable(grid_mapping).attributes) else: - #logger.info('grid_mapping missing variable for: ' f'{grid_mapping}') + # logger.info('grid_mapping missing variable for: ' f'{grid_mapping}') cf = varinfo.get_missing_variable_attributes(grid_mapping) - #logger.info('cf attr for missing variable for: ' f'{grid_mapping}' f'{cf}') + # logger.info('cf attr for missing variable for: ' f'{grid_mapping}' f'{cf}') crs = CRS.from_cf(cf) logger.info('crs missing variable for: ' f'{grid_mapping}' f'{crs}') except AttributeError as exception: - #check if there is a CF override - crs = CRS.from_cf(varinfo.get_missing_variable_attributes(grid_mapping)) - if crs is None: - raise MissingGridMappingVariable(grid_mapping, variable) from exception - - #check if there is a CF override + # check if there is a CF override + crs = CRS.from_cf(varinfo.get_missing_variable_attributes(grid_mapping)) + if crs is None: + raise MissingGridMappingVariable(grid_mapping, variable) from exception + + # check if there is a CF override else: variable1 = varinfo.get_variable(variable) logger.info('variable-name:' f'{variable} ', 'variable:' f'{variable1.name}') diff --git a/hoss/spatial.py b/hoss/spatial.py index 475756b..827de3d 100644 --- a/hoss/spatial.py +++ b/hoss/spatial.py @@ -22,8 +22,9 @@ """ -from typing import List, Set from logging import Logger +from typing import List, Set + from harmony.message import Message from netCDF4 import Dataset from numpy.ma.core import MaskedArray @@ -41,16 +42,16 @@ get_dimension_bounds, get_dimension_extents, get_dimension_index_range, - update_dimension_variables + update_dimension_variables, ) from hoss.projection_utilities import ( get_projected_x_y_extents, get_projected_x_y_variables, get_variable_crs, ) - from hoss.utilities import format_dictionary_string + def get_spatial_index_ranges( required_variables: Set[str], required_dimensions: Set[str], @@ -93,11 +94,17 @@ def get_spatial_index_ranges( non_spatial_variables = required_variables.difference( varinfo.get_spatial_dimensions(required_variables) ) - logger.info('bounding_box: ' f'{bounding_box}' - '\r\n geo dims:' f'{geographic_dimensions}' - '\r\n proj dims:' f'{projected_dimensions}' - '\r\n non dims:' f'{non_spatial_variables}') - + logger.info( + 'bounding_box: ' + f'{bounding_box}' + '\r\n geo dims:' + f'{geographic_dimensions}' + '\r\n proj dims:' + f'{projected_dimensions}' + '\r\n non dims:' + f'{non_spatial_variables}' + ) + with Dataset(dimensions_path, 'r') as dimensions_file: if len(geographic_dimensions) > 0: # If there is no bounding box, but there is a shape file, calculate @@ -111,7 +118,6 @@ def get_spatial_index_ranges( dimension, varinfo, dimensions_file, bounding_box ) return index_ranges - elif len(projected_dimensions) > 0: for non_spatial_variable in non_spatial_variables: index_ranges.update( @@ -126,7 +132,7 @@ def get_spatial_index_ranges( ) ) return index_ranges - + elif len(required_dimensions) > 0: for non_spatial_variable in non_spatial_variables: index_ranges.update( @@ -141,7 +147,12 @@ def get_spatial_index_ranges( shape_file_path=shape_file_path, ) ) - logger.info('non_spatial_variable:' f'{non_spatial_variable}' ',index_ranges:' f'{format_dictionary_string(index_ranges)}') + logger.info( + 'non_spatial_variable:' + f'{non_spatial_variable}' + ',index_ranges:' + f'{format_dictionary_string(index_ranges)}' + ) return index_ranges @@ -180,9 +191,9 @@ def get_projected_x_y_index_ranges( and projected_y is not None and not set((projected_x, projected_y)).issubset(set(index_ranges.keys())) ): - crs = get_variable_crs(non_spatial_variable, varinfo,logger) + crs = get_variable_crs(non_spatial_variable, varinfo, logger) logger.info('crs=' f'{crs}') - + x_y_extents = get_projected_x_y_extents( dimensions_file[projected_x][:], dimensions_file[projected_y][:], @@ -209,12 +220,13 @@ def get_projected_x_y_index_ranges( ) x_y_index_ranges = {projected_x: x_index_ranges, projected_y: y_index_ranges} - + else: x_y_index_ranges = {} return x_y_index_ranges + def get_required_x_y_index_ranges( non_spatial_variable: str, varinfo: VarInfoFromDmr, @@ -250,44 +262,45 @@ def get_required_x_y_index_ranges( varinfo, logger, ) - crs = get_variable_crs(non_spatial_variable, varinfo,logger) - + crs = get_variable_crs(non_spatial_variable, varinfo, logger) + x_y_extents = get_projected_x_y_extents( - dimensions_file[projected_x][:], - dimensions_file[projected_y][:], - #xdims, - #ydims, - crs, - shape_file=shape_file_path, - bounding_box=bounding_box, - ) + dimensions_file[projected_x][:], + dimensions_file[projected_y][:], + # xdims, + # ydims, + crs, + shape_file=shape_file_path, + bounding_box=bounding_box, + ) logger.info('x_y_extents:' f'{format_dictionary_string(x_y_extents)}') x_index_ranges = get_dimension_index_range( - dimensions_file[projected_x][:], - #xdims, - x_y_extents['x_min'], - x_y_extents['x_max'], - #bounds_values=x_bounds, - ) + dimensions_file[projected_x][:], + # xdims, + x_y_extents['x_min'], + x_y_extents['x_max'], + # bounds_values=x_bounds, + ) logger.info('x_index_ranges: ' f'{x_index_ranges}') - + y_index_ranges = get_dimension_index_range( - dimensions_file[projected_y][:], - #ydims, - x_y_extents['y_min'], - x_y_extents['y_max'], - #bounds_values=y_bounds, - ) + dimensions_file[projected_y][:], + # ydims, + x_y_extents['y_min'], + x_y_extents['y_max'], + # bounds_values=y_bounds, + ) logger.info('y_index_ranges: ' f'{y_index_ranges}') x_y_index_ranges = {projected_y: y_index_ranges, projected_x: x_index_ranges} logger.info('x_y_index_ranges-str:' f'{format_dictionary_string(x_y_index_ranges)}') logger.info('x_y_index_ranges: ' f'{x_y_index_ranges}') - + return x_y_index_ranges + def get_geographic_index_range( dimension: str, varinfo: VarInfoFromDmr, diff --git a/hoss/subset.py b/hoss/subset.py index c8043be..e8d154e 100644 --- a/hoss/subset.py +++ b/hoss/subset.py @@ -27,11 +27,14 @@ ) from hoss.spatial import get_spatial_index_ranges from hoss.temporal import get_temporal_index_ranges -from hoss.utilities import (download_url, - format_variable_set_string, - get_opendap_nc4, - format_dictionary_string +from hoss.utilities import ( + download_url, + format_dictionary_string, + format_variable_set_string, + get_opendap_nc4, ) + + def subset_granule( opendap_url: str, harmony_source: Source, @@ -125,12 +128,14 @@ def subset_granule( harmony_message, output_dir, logger, config ) logger.info( - 'All required variables: ' - f'{format_variable_set_string(required_variables)}' - ', dimensions_path: ' f'{dimensions_path}' - ', shapefilepath:' f'{shape_file_path}' + 'All required variables: ' + f'{format_variable_set_string(required_variables)}' + ', dimensions_path: ' + f'{dimensions_path}' + ', shapefilepath:' + f'{shape_file_path}' ) - + index_ranges.update( get_spatial_index_ranges( required_variables, @@ -142,8 +147,10 @@ def subset_granule( shape_file_path, ) ) - logger.info('subset_granule - index_ranges:' f'{format_dictionary_string(index_ranges)}') - + logger.info( + 'subset_granule - index_ranges:' f'{format_dictionary_string(index_ranges)}' + ) + if harmony_message.temporal is not None: # Update `index_ranges` cache with ranges for temporal # variables. This will convert information from the temporal range From c4abf26c7f20cd0577cf5e95273b861e5a48b258 Mon Sep 17 00:00:00 2001 From: sudhamurthy Date: Mon, 9 Sep 2024 19:57:38 -0400 Subject: [PATCH 03/41] DAS-2232 - updates to correct unit test failures --- hoss/dimension_utilities.py | 45 +++------------------------------ hoss/projection_utilities.py | 35 ++++++++------------------ hoss/spatial.py | 49 +++--------------------------------- hoss/subset.py | 5 ++-- 4 files changed, 19 insertions(+), 115 deletions(-) diff --git a/hoss/dimension_utilities.py b/hoss/dimension_utilities.py index 3235dc8..aca0079 100644 --- a/hoss/dimension_utilities.py +++ b/hoss/dimension_utilities.py @@ -118,7 +118,6 @@ def update_dimension_variables( prefetch_dataset: Dataset, required_dimensions: Set[str], varinfo: VarInfoFromDmr, - logger: Logger, ) -> Dict[str, ndarray]: """Augment a NetCDF4 file with artificial 1D dimensions variable for each 2D dimension variable" @@ -133,19 +132,13 @@ def update_dimension_variables( """ for dimension_name in required_dimensions: dimension_variable = varinfo.get_variable(dimension_name) - logger.info('dimension name: ' f'{dimension_name}') - logger.info('dimension path:' f'{dimension_variable.full_name_path}') - - if is_variable_one_dimensional(prefetch_dataset, dimension_variable): - logger.info('No changes needed: ' f'{dimension_name}') - else: + if not is_variable_one_dimensional(prefetch_dataset, dimension_variable): col_size = prefetch_dataset[dimension_variable.full_name_path][:].shape[0] row_size = prefetch_dataset[dimension_variable.full_name_path][:].shape[1] - crs = get_variable_crs(dimension_name, varinfo, logger) - logger.info('row_size=' f'{row_size}' ',col_size=' f'{col_size}') + crs = get_variable_crs(dimension_name, varinfo) geo_grid_corners = get_geo_grid_corners( - prefetch_dataset, required_dimensions, varinfo, logger + prefetch_dataset, required_dimensions, varinfo ) x_y_extents = get_x_y_extents_from_geographic_points(geo_grid_corners, crs) @@ -158,21 +151,6 @@ def update_dimension_variables( x_resolution = (x_max - x_min) / row_size y_resolution = (y_max - y_min) / col_size - logger.info( - 'x_min:' - f'{x_min}' - ',x_max=' - f'{x_max}' - ',y_min:' - f'{y_min}' - ',y_max=' - f'{y_max}' - ',x_res=' - f'{x_resolution}' - 'y_res=' - f'{y_resolution}' - ) - # create the xy dim scales x_dim = np.arange(x_min, x_max, x_resolution) @@ -185,7 +163,6 @@ def get_geo_grid_corners( prefetch_dataset: Dataset, required_dimensions: Set[str], varinfo: VarInfoFromDmr, - logger: Logger, ) -> list[Tuple[float]]: """ This method is used to return the lat lon corners from a 2D @@ -233,17 +210,6 @@ def get_geo_grid_corners( bottomright_corner, bottomleft_corner, ] - logger.info( - 'topleft:' - f'{topleft_corner}' - 'topright:' - f'{topright_corner}' - ',bottomright:' - f'{bottomright_corner}' - ',bottomleft:' - f'{bottomleft_corner}' - ) - return geo_grid_corners @@ -554,7 +520,6 @@ def add_index_range( variable_name: str, varinfo: VarInfoFromDmr, index_ranges: IndexRanges, - logger: Logger, ) -> str: """Append the index ranges of each dimension for the specified variable. If there are no dimensions with listed index ranges, then the full @@ -567,14 +532,10 @@ def add_index_range( """ variable = varinfo.get_variable(variable_name) - logger.info('variable name:' f'{variable_name}') range_strings = [] if variable.dimensions == []: for dimension in index_ranges.keys(): dimension_range = index_ranges.get(dimension) - logger.info( - 'dimension=' f'{dimension}' ', dimension_range=' f'{dimension_range}' - ) if dimension_range is not None and dimension_range[0] <= dimension_range[1]: range_strings.append(f'[{dimension_range[0]}:{dimension_range[1]}]') else: diff --git a/hoss/projection_utilities.py b/hoss/projection_utilities.py index 7924ed6..855a2b5 100644 --- a/hoss/projection_utilities.py +++ b/hoss/projection_utilities.py @@ -41,7 +41,7 @@ Shape = Union[LineString, Point, Polygon, MultiShape] -def get_variable_crs(variable: str, varinfo: VarInfoFromDmr, logger: Logger) -> CRS: +def get_variable_crs(variable: str, varinfo: VarInfoFromDmr) -> CRS: """Check the metadata attributes for the variable to find the associated grid mapping variable. Create a `pyproj.CRS` object from the grid mapping variable metadata attributes. @@ -58,33 +58,20 @@ def get_variable_crs(variable: str, varinfo: VarInfoFromDmr, logger: Logger) -> if grid_mapping is not None: try: - logger.info('grid_mapping first try: ' f'{grid_mapping}') grid_mapping_variable = varinfo.get_variable(grid_mapping) - if grid_mapping_variable is not None: - crs = CRS.from_cf(varinfo.get_variable(grid_mapping).attributes) - else: - # logger.info('grid_mapping missing variable for: ' f'{grid_mapping}') - cf = varinfo.get_missing_variable_attributes(grid_mapping) - # logger.info('cf attr for missing variable for: ' f'{grid_mapping}' f'{cf}') - crs = CRS.from_cf(cf) - logger.info('crs missing variable for: ' f'{grid_mapping}' f'{crs}') + if grid_mapping_variable is None: + # check for any overrides + cf_attributes = varinfo.get_missing_variable_attributes(grid_mapping) + if len(cf_attributes) != 0: + crs = CRS.from_cf(cf_attributes) + return crs + crs = CRS.from_cf(varinfo.get_variable(grid_mapping).attributes) + except AttributeError as exception: - # check if there is a CF override - crs = CRS.from_cf(varinfo.get_missing_variable_attributes(grid_mapping)) - if crs is None: - raise MissingGridMappingVariable(grid_mapping, variable) from exception + raise MissingGridMappingVariable(grid_mapping, variable) from exception - # check if there is a CF override else: - variable1 = varinfo.get_variable(variable) - logger.info('variable-name:' f'{variable} ', 'variable:' f'{variable1.name}') - grid_mapping = variable1.attributes.get('grid_mapping') - logger.info('grid_mapping second try: ' f'{grid_mapping}') - cf = varinfo.get_missing_variable_attributes(grid_mapping) - logger.info('cf: ' f'{cf}') - crs = CRS.from_cf(cf) - if crs is None: - raise MissingGridMappingMetadata(variable) + raise MissingGridMappingMetadata(variable) return crs diff --git a/hoss/spatial.py b/hoss/spatial.py index 827de3d..14628ab 100644 --- a/hoss/spatial.py +++ b/hoss/spatial.py @@ -54,12 +54,11 @@ def get_spatial_index_ranges( required_variables: Set[str], - required_dimensions: Set[str], varinfo: VarInfoFromDmr, dimensions_path: str, harmony_message: Message, - logger: Logger, shape_file_path: str = None, + required_dimensions: Set[str] = set(), ) -> IndexRanges: """Return a dictionary containing indices that correspond to the minimum and maximum extents for all horizontal spatial coordinate variables @@ -94,16 +93,6 @@ def get_spatial_index_ranges( non_spatial_variables = required_variables.difference( varinfo.get_spatial_dimensions(required_variables) ) - logger.info( - 'bounding_box: ' - f'{bounding_box}' - '\r\n geo dims:' - f'{geographic_dimensions}' - '\r\n proj dims:' - f'{projected_dimensions}' - '\r\n non dims:' - f'{non_spatial_variables}' - ) with Dataset(dimensions_path, 'r') as dimensions_file: if len(geographic_dimensions) > 0: @@ -126,7 +115,6 @@ def get_spatial_index_ranges( varinfo, dimensions_file, index_ranges, - logger, bounding_box=bounding_box, shape_file_path=shape_file_path, ) @@ -142,17 +130,10 @@ def get_spatial_index_ranges( dimensions_file, required_dimensions, index_ranges, - logger, bounding_box=bounding_box, shape_file_path=shape_file_path, ) ) - logger.info( - 'non_spatial_variable:' - f'{non_spatial_variable}' - ',index_ranges:' - f'{format_dictionary_string(index_ranges)}' - ) return index_ranges @@ -161,7 +142,6 @@ def get_projected_x_y_index_ranges( varinfo: VarInfoFromDmr, dimensions_file: Dataset, index_ranges: IndexRanges, - logger: Logger, bounding_box: BBox = None, shape_file_path: str = None, ) -> IndexRanges: @@ -191,8 +171,7 @@ def get_projected_x_y_index_ranges( and projected_y is not None and not set((projected_x, projected_y)).issubset(set(index_ranges.keys())) ): - crs = get_variable_crs(non_spatial_variable, varinfo, logger) - logger.info('crs=' f'{crs}') + crs = get_variable_crs(non_spatial_variable, varinfo) x_y_extents = get_projected_x_y_extents( dimensions_file[projected_x][:], @@ -201,26 +180,21 @@ def get_projected_x_y_index_ranges( shape_file=shape_file_path, bounding_box=bounding_box, ) - x_bounds = get_dimension_bounds(projected_x, varinfo, dimensions_file) y_bounds = get_dimension_bounds(projected_y, varinfo, dimensions_file) - x_index_ranges = get_dimension_index_range( dimensions_file[projected_x][:], x_y_extents['x_min'], x_y_extents['x_max'], bounds_values=x_bounds, ) - y_index_ranges = get_dimension_index_range( dimensions_file[projected_y][:], x_y_extents['y_min'], x_y_extents['y_max'], bounds_values=y_bounds, ) - x_y_index_ranges = {projected_x: x_index_ranges, projected_y: y_index_ranges} - else: x_y_index_ranges = {} @@ -233,7 +207,6 @@ def get_required_x_y_index_ranges( coordinates_file: Dataset, required_dimensions: Set[str], index_ranges: IndexRanges, - logger: Logger, bounding_box: BBox = None, shape_file_path: str = None, ) -> IndexRanges: @@ -260,44 +233,28 @@ def get_required_x_y_index_ranges( coordinates_file, required_dimensions, varinfo, - logger, ) - crs = get_variable_crs(non_spatial_variable, varinfo, logger) + crs = get_variable_crs(non_spatial_variable, varinfo) x_y_extents = get_projected_x_y_extents( dimensions_file[projected_x][:], dimensions_file[projected_y][:], - # xdims, - # ydims, crs, shape_file=shape_file_path, bounding_box=bounding_box, ) - logger.info('x_y_extents:' f'{format_dictionary_string(x_y_extents)}') - x_index_ranges = get_dimension_index_range( dimensions_file[projected_x][:], - # xdims, x_y_extents['x_min'], x_y_extents['x_max'], - # bounds_values=x_bounds, ) - logger.info('x_index_ranges: ' f'{x_index_ranges}') - y_index_ranges = get_dimension_index_range( dimensions_file[projected_y][:], - # ydims, x_y_extents['y_min'], x_y_extents['y_max'], - # bounds_values=y_bounds, ) - logger.info('y_index_ranges: ' f'{y_index_ranges}') x_y_index_ranges = {projected_y: y_index_ranges, projected_x: x_index_ranges} - - logger.info('x_y_index_ranges-str:' f'{format_dictionary_string(x_y_index_ranges)}') - logger.info('x_y_index_ranges: ' f'{x_y_index_ranges}') - return x_y_index_ranges diff --git a/hoss/subset.py b/hoss/subset.py index e8d154e..8398fd3 100644 --- a/hoss/subset.py +++ b/hoss/subset.py @@ -139,12 +139,11 @@ def subset_granule( index_ranges.update( get_spatial_index_ranges( required_variables, - required_dimensions, varinfo, dimensions_path, harmony_message, - logger, shape_file_path, + required_dimensions, ) ) logger.info( @@ -163,7 +162,7 @@ def subset_granule( # Add any range indices to variable names for DAP4 constraint expression. variables_with_ranges = set( - add_index_range(variable, varinfo, index_ranges, logger) + add_index_range(variable, varinfo, index_ranges) for variable in required_variables ) logger.info( From be280f08388f07c65e07b6c739d105560a75d072 Mon Sep 17 00:00:00 2001 From: sudhamurthy Date: Mon, 9 Sep 2024 23:40:04 -0400 Subject: [PATCH 04/41] DAS-2232 updates for unit test failures --- hoss/dimension_utilities.py | 31 +++++++++++++++++----------- hoss/projection_utilities.py | 1 - hoss/spatial.py | 39 +++++++++++++++++------------------- hoss/subset.py | 3 +-- 4 files changed, 38 insertions(+), 36 deletions(-) diff --git a/hoss/dimension_utilities.py b/hoss/dimension_utilities.py index aca0079..e338ced 100644 --- a/hoss/dimension_utilities.py +++ b/hoss/dimension_utilities.py @@ -23,16 +23,12 @@ from numpy.ma.core import MaskedArray from varinfo import VariableFromDmr, VarInfoFromDmr -from hoss.bbox_utilities import flatten_list from hoss.exceptions import InvalidNamedDimension, InvalidRequestedRange from hoss.projection_utilities import ( - get_projected_x_y_extents, - get_projected_x_y_variables, get_variable_crs, get_x_y_extents_from_geographic_points, ) from hoss.utilities import ( - format_dictionary_string, format_variable_set_string, get_opendap_nc4, get_value_or_default, @@ -63,6 +59,22 @@ def is_index_subset(message: Message) -> bool: ) +def get_override_dimensions( + varinfo: VarInfoFromDmr, + required_variables: Set[str], +) -> set[str]: + """Determine the dimensions that need to be "pre-fetched" from OPeNDAP + If dimensions are not available, get variables that can be + used to generate dimensions + """ + # check if coordinate variables are provided + # this should be be a configurable in hoss_config.json + override_dimensions = varinfo.get_references_for_attribute( + required_variables, 'coordinates' + ) + return override_dimensions + + def prefetch_dimension_variables( opendap_url: str, varinfo: VarInfoFromDmr, @@ -71,7 +83,7 @@ def prefetch_dimension_variables( logger: Logger, access_token: str, config: Config, -) -> tuple[str, set[str]]: +) -> tuple[str, set[str]] | str: """Determine the dimensions that need to be "pre-fetched" from OPeNDAP in order to derive index ranges upon them. Initially, this was just spatial and temporal dimensions, but to support generic dimension @@ -82,10 +94,7 @@ def prefetch_dimension_variables( """ required_dimensions = varinfo.get_required_dimensions(required_variables) if len(required_dimensions) == 0: - # check if coordinate variables are provided - required_dimensions = varinfo.get_references_for_attribute( - required_variables, 'coordinates' - ) + required_dimensions = get_override_dimensions(varinfo, required_variables) logger.info('coordinates: ' f'{required_dimensions}') bounds = varinfo.get_references_for_attribute(required_dimensions, 'bounds') @@ -96,14 +105,12 @@ def prefetch_dimension_variables( 'Variables being retrieved in prefetch request: ' f'{format_variable_set_string(required_dimensions)}' ) - required_dimensions_nc4 = get_opendap_nc4( opendap_url, required_dimensions, output_dir, logger, access_token, config ) - # Create bounds variables if necessary. add_bounds_variables(required_dimensions_nc4, required_dimensions, varinfo, logger) - return required_dimensions_nc4, required_dimensions + return required_dimensions_nc4 def is_variable_one_dimensional( diff --git a/hoss/projection_utilities.py b/hoss/projection_utilities.py index 855a2b5..8513b67 100644 --- a/hoss/projection_utilities.py +++ b/hoss/projection_utilities.py @@ -11,7 +11,6 @@ """ import json -from logging import Logger from typing import Dict, List, Optional, Tuple, Union, get_args import numpy as np diff --git a/hoss/spatial.py b/hoss/spatial.py index 14628ab..69a02a3 100644 --- a/hoss/spatial.py +++ b/hoss/spatial.py @@ -22,7 +22,6 @@ """ -from logging import Logger from typing import List, Set from harmony.message import Message @@ -42,6 +41,7 @@ get_dimension_bounds, get_dimension_extents, get_dimension_index_range, + get_override_dimensions, update_dimension_variables, ) from hoss.projection_utilities import ( @@ -49,7 +49,6 @@ get_projected_x_y_variables, get_variable_crs, ) -from hoss.utilities import format_dictionary_string def get_spatial_index_ranges( @@ -58,7 +57,6 @@ def get_spatial_index_ranges( dimensions_path: str, harmony_message: Message, shape_file_path: str = None, - required_dimensions: Set[str] = set(), ) -> IndexRanges: """Return a dictionary containing indices that correspond to the minimum and maximum extents for all horizontal spatial coordinate variables @@ -106,7 +104,6 @@ def get_spatial_index_ranges( index_ranges[dimension] = get_geographic_index_range( dimension, varinfo, dimensions_file, bounding_box ) - return index_ranges elif len(projected_dimensions) > 0: for non_spatial_variable in non_spatial_variables: index_ranges.update( @@ -119,22 +116,22 @@ def get_spatial_index_ranges( shape_file_path=shape_file_path, ) ) - return index_ranges - - elif len(required_dimensions) > 0: - for non_spatial_variable in non_spatial_variables: - index_ranges.update( - get_required_x_y_index_ranges( - non_spatial_variable, - varinfo, - dimensions_file, - required_dimensions, - index_ranges, - bounding_box=bounding_box, - shape_file_path=shape_file_path, + else: + override_dimensions = get_override_dimensions(varinfo, required_variables) + if len(override_dimensions) > 0: + for non_spatial_variable in non_spatial_variables: + index_ranges.update( + get_required_x_y_index_ranges( + non_spatial_variable, + varinfo, + dimensions_file, + override_dimensions, + index_ranges, + bounding_box=bounding_box, + shape_file_path=shape_file_path, + ) ) - ) - return index_ranges + return index_ranges def get_projected_x_y_index_ranges( @@ -205,7 +202,7 @@ def get_required_x_y_index_ranges( non_spatial_variable: str, varinfo: VarInfoFromDmr, coordinates_file: Dataset, - required_dimensions: Set[str], + override_dimensions: Set[str], index_ranges: IndexRanges, bounding_box: BBox = None, shape_file_path: str = None, @@ -231,7 +228,7 @@ def get_required_x_y_index_ranges( projected_y = 'projected_y' dimensions_file = update_dimension_variables( coordinates_file, - required_dimensions, + override_dimensions, varinfo, ) crs = get_variable_crs(non_spatial_variable, varinfo) diff --git a/hoss/subset.py b/hoss/subset.py index 8398fd3..80c8e4d 100644 --- a/hoss/subset.py +++ b/hoss/subset.py @@ -92,7 +92,7 @@ def subset_granule( if request_is_index_subset: # Prefetch all dimension variables in full: - dimensions_path, required_dimensions = prefetch_dimension_variables( + dimensions_path = prefetch_dimension_variables( opendap_url, varinfo, required_variables, @@ -143,7 +143,6 @@ def subset_granule( dimensions_path, harmony_message, shape_file_path, - required_dimensions, ) ) logger.info( From 2a48f93337fae64ed3b5d804317076d2550b98d3 Mon Sep 17 00:00:00 2001 From: sudhamurthy Date: Tue, 10 Sep 2024 14:09:59 -0400 Subject: [PATCH 05/41] DAS-2232 - all unit tests pass --- hoss/dimension_utilities.py | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/hoss/dimension_utilities.py b/hoss/dimension_utilities.py index e338ced..ae1b287 100644 --- a/hoss/dimension_utilities.py +++ b/hoss/dimension_utilities.py @@ -69,10 +69,13 @@ def get_override_dimensions( """ # check if coordinate variables are provided # this should be be a configurable in hoss_config.json - override_dimensions = varinfo.get_references_for_attribute( - required_variables, 'coordinates' - ) - return override_dimensions + try: + override_dimensions = varinfo.get_references_for_attribute( + required_variables, 'coordinates' + ) + return override_dimensions + except AttributeError as exception: + return set() def prefetch_dimension_variables( @@ -540,14 +543,20 @@ def add_index_range( """ variable = varinfo.get_variable(variable_name) range_strings = [] - if variable.dimensions == []: - for dimension in index_ranges.keys(): - dimension_range = index_ranges.get(dimension) - if dimension_range is not None and dimension_range[0] <= dimension_range[1]: - range_strings.append(f'[{dimension_range[0]}:{dimension_range[1]}]') - else: - range_strings.append('[]') + if variable.dimensions == []: + override_dimensions = get_override_dimensions(varinfo, variable_name) + if len(override_dimensions) > 0: + for dimension in override_dimensions: + dimension_range = index_ranges.get(dimension) + + if ( + dimension_range is not None + and dimension_range[0] <= dimension_range[1] + ): + range_strings.append(f'[{dimension_range[0]}:{dimension_range[1]}]') + else: + range_strings.append('[]') else: for dimension in variable.dimensions: dimension_range = index_ranges.get(dimension) From 2c93a4b194d32fc75497f013167ab193129e1c9f Mon Sep 17 00:00:00 2001 From: sudhamurthy Date: Tue, 10 Sep 2024 14:27:35 -0400 Subject: [PATCH 06/41] DAS-2232 updates to version number --- CHANGELOG.md | 6 ++++++ docker/service_version.txt | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a18ed7..6c34d25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## v1.1.0 +### 2024-09-10 + +This version of HOSS provides support for products without CF compliance like SMAP L3 +Methods added to get dimension scales from coordinate attributes and grid mapping with overrides +in the json file ## v1.0.5 ### 2024-08-19 diff --git a/docker/service_version.txt b/docker/service_version.txt index 90a27f9..9084fa2 100644 --- a/docker/service_version.txt +++ b/docker/service_version.txt @@ -1 +1 @@ -1.0.5 +1.1.0 From dfb1e157da57cebf72781f876f46e7600c06ed05 Mon Sep 17 00:00:00 2001 From: sudhamurthy Date: Tue, 10 Sep 2024 17:30:26 -0400 Subject: [PATCH 07/41] DAS-2232 - updated notebook version due to Synk vulnerability. Also removed duplicate method --- docs/requirements.txt | 2 +- hoss/dimension_utilities.py | 7 +++- hoss/spatial.py | 79 ++++++++----------------------------- 3 files changed, 22 insertions(+), 66 deletions(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index dd7a29c..700910c 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -16,5 +16,5 @@ # harmony-py~=0.4.10 netCDF4~=1.6.4 -notebook~=7.0.4 +notebook~=7.2.2 xarray~=2023.9.0 diff --git a/hoss/dimension_utilities.py b/hoss/dimension_utilities.py index ae1b287..758c37b 100644 --- a/hoss/dimension_utilities.py +++ b/hoss/dimension_utilities.py @@ -74,7 +74,7 @@ def get_override_dimensions( required_variables, 'coordinates' ) return override_dimensions - except AttributeError as exception: + except AttributeError: return set() @@ -682,7 +682,10 @@ def get_dimension_bounds( be returned. """ - bounds = varinfo.get_variable(dimension_name).references.get('bounds') + try: + bounds = varinfo.get_variable(dimension_name).references.get('bounds') + except AttributeError: + bounds = None if bounds is not None: try: diff --git a/hoss/spatial.py b/hoss/spatial.py index 69a02a3..8a88dfe 100644 --- a/hoss/spatial.py +++ b/hoss/spatial.py @@ -121,14 +121,14 @@ def get_spatial_index_ranges( if len(override_dimensions) > 0: for non_spatial_variable in non_spatial_variables: index_ranges.update( - get_required_x_y_index_ranges( + get_projected_x_y_index_ranges( non_spatial_variable, varinfo, dimensions_file, - override_dimensions, index_ranges, bounding_box=bounding_box, shape_file_path=shape_file_path, + override_dimensions=override_dimensions, ) ) return index_ranges @@ -141,6 +141,7 @@ def get_projected_x_y_index_ranges( index_ranges: IndexRanges, bounding_box: BBox = None, shape_file_path: str = None, + override_dimensions: Set[str] = set(), ) -> IndexRanges: """This function returns a dictionary containing the minimum and maximum index ranges for a pair of projection x and y coordinates, e.g.: @@ -159,10 +160,19 @@ def get_projected_x_y_index_ranges( projected coordinate points. """ - projected_x, projected_y = get_projected_x_y_variables( - varinfo, non_spatial_variable - ) - + if len(override_dimensions) == 0: + projected_x, projected_y = get_projected_x_y_variables( + varinfo, non_spatial_variable + ) + else: + projected_x = 'projected_x' + projected_y = 'projected_y' + override_dimensions_file = update_dimension_variables( + dimensions_file, + override_dimensions, + varinfo, + ) + dimensions_file = override_dimensions_file if ( projected_x is not None and projected_y is not None @@ -198,63 +208,6 @@ def get_projected_x_y_index_ranges( return x_y_index_ranges -def get_required_x_y_index_ranges( - non_spatial_variable: str, - varinfo: VarInfoFromDmr, - coordinates_file: Dataset, - override_dimensions: Set[str], - index_ranges: IndexRanges, - bounding_box: BBox = None, - shape_file_path: str = None, -) -> IndexRanges: - """This function returns a dictionary containing the minimum and maximum - index ranges for a pair of projection x and y coordinates, e.g.: - - index_ranges = {'/x': (20, 42), '/y': (31, 53)} - - First, the dimensions of the input, non-spatial variable are checked - for associated projection x and y coordinates. If these are present, - and they have not already been added to the `index_ranges` cache, the - extents of the input spatial subset are determined in these projected - coordinates. This requires the derivation of a minimum resolution of - the target grid in geographic coordinates. Points must be placed along - the exterior of the spatial subset shape. All points are then projected - from a geographic Coordinate Reference System (CRS) to the target grid - CRS. The minimum and maximum values are then derived from these - projected coordinate points. - - """ - projected_x = 'projected_x' - projected_y = 'projected_y' - dimensions_file = update_dimension_variables( - coordinates_file, - override_dimensions, - varinfo, - ) - crs = get_variable_crs(non_spatial_variable, varinfo) - - x_y_extents = get_projected_x_y_extents( - dimensions_file[projected_x][:], - dimensions_file[projected_y][:], - crs, - shape_file=shape_file_path, - bounding_box=bounding_box, - ) - x_index_ranges = get_dimension_index_range( - dimensions_file[projected_x][:], - x_y_extents['x_min'], - x_y_extents['x_max'], - ) - y_index_ranges = get_dimension_index_range( - dimensions_file[projected_y][:], - x_y_extents['y_min'], - x_y_extents['y_max'], - ) - - x_y_index_ranges = {projected_y: y_index_ranges, projected_x: x_index_ranges} - return x_y_index_ranges - - def get_geographic_index_range( dimension: str, varinfo: VarInfoFromDmr, From e2ff61f6b6497abebcdc4e731f2db2bd0a46e467 Mon Sep 17 00:00:00 2001 From: sudhamurthy Date: Fri, 13 Sep 2024 01:07:13 -0400 Subject: [PATCH 08/41] DAS-2232 fixed spatial subsetting bugs introduced when removing duplicate nethods --- hoss/dimension_utilities.py | 40 ++++++++++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/hoss/dimension_utilities.py b/hoss/dimension_utilities.py index 758c37b..cb6ca7c 100644 --- a/hoss/dimension_utilities.py +++ b/hoss/dimension_utilities.py @@ -59,6 +59,23 @@ def is_index_subset(message: Message) -> bool: ) +def get_override_projected_dimensions( + varinfo: VarInfoFromDmr, + override_variable_name: str, +) -> str: + """returns the x-y projection variable names that would + match the geo coordinate names + + """ + projection_variable_name = None + override_variable = varinfo.get_variable(override_variable_name) + if override_variable.is_latitude(): + projection_variable_name = 'projected_y' + elif override_variable.is_longitude(): + projection_variable_name = 'projected_x' + return projection_variable_name + + def get_override_dimensions( varinfo: VarInfoFromDmr, required_variables: Set[str], @@ -543,13 +560,12 @@ def add_index_range( """ variable = varinfo.get_variable(variable_name) range_strings = [] - if variable.dimensions == []: - override_dimensions = get_override_dimensions(varinfo, variable_name) + override_dimensions = get_override_dimensions(varinfo, [variable_name]) if len(override_dimensions) > 0: - for dimension in override_dimensions: + for override in reversed(list(override_dimensions)): + dimension = get_override_projected_dimensions(varinfo, override) dimension_range = index_ranges.get(dimension) - if ( dimension_range is not None and dimension_range[0] <= dimension_range[1] @@ -557,6 +573,21 @@ def add_index_range( range_strings.append(f'[{dimension_range[0]}:{dimension_range[1]}]') else: range_strings.append('[]') + else: + # if the override is the variable + override = get_override_projected_dimensions(varinfo, variable_name) + if override is not None and override in index_ranges.keys(): + for dimension in reversed(index_ranges.keys()): + dimension_range = index_ranges.get(dimension) + if ( + dimension_range is not None + and dimension_range[0] <= dimension_range[1] + ): + range_strings.append( + f'[{dimension_range[0]}:{dimension_range[1]}]' + ) + else: + range_strings.append('[]') else: for dimension in variable.dimensions: dimension_range = index_ranges.get(dimension) @@ -570,7 +601,6 @@ def add_index_range( indices_string = '' else: indices_string = ''.join(range_strings) - return f'{variable_name}{indices_string}' From 756f7c05d69fc135b5fc39825f5d7e1dde5daada Mon Sep 17 00:00:00 2001 From: sudhamurthy Date: Thu, 26 Sep 2024 03:04:38 -0400 Subject: [PATCH 09/41] DAS-2232 initial commit after PR feedback --- CHANGELOG.md | 15 +- hoss/dimension_utilities.py | 217 ++++++++++++++----------- hoss/exceptions.py | 30 ++++ hoss/projection_utilities.py | 4 +- hoss/spatial.py | 15 +- hoss/subset.py | 15 +- tests/unit/test_dimension_utilities.py | 8 +- tests/unit/test_subset.py | 22 +-- 8 files changed, 190 insertions(+), 136 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c34d25..d24dc19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,18 @@ ## v1.1.0 ### 2024-09-10 -This version of HOSS provides support for products without CF compliance like SMAP L3 -Methods added to get dimension scales from coordinate attributes and grid mapping with overrides -in the json file +This version of HOSS provides support for products that do not comply with CF standards like SMAP L3 +New methods added to retrieve dimension scales from coordinate attributes and grid mappings, using +overrides specified in the hoss_config.json configuration file. `get_coordinate_variables' gets coordinate +datasets when the dimension scales are not present in the source file. The prefetch gets the coordinate +datasets during prefetch when the dimension scales are not present. `is_variable_one_dimensional' function +checks the dimensionality of the coordinate datasets. `update_dimension_variables' gets a row and column +from the 2D datasets to 1D and uses the crs attribute to get the projection of the granule to convert the +lat/lon array to projected x-y dimension scales. `get_override_projected_dimensions` provides the projected +dimension scale names after the conversion. The `get_variable_crs' also updated when the +grid mapping variable does not exist in the granule and an override is provided in an updated hoss_config.json + +`get_override_projection_dimensions` ## v1.0.5 ### 2024-08-19 diff --git a/hoss/dimension_utilities.py b/hoss/dimension_utilities.py index cb6ca7c..11e8cdf 100644 --- a/hoss/dimension_utilities.py +++ b/hoss/dimension_utilities.py @@ -23,7 +23,12 @@ from numpy.ma.core import MaskedArray from varinfo import VariableFromDmr, VarInfoFromDmr -from hoss.exceptions import InvalidNamedDimension, InvalidRequestedRange +from hoss.exceptions import ( + InvalidNamedDimension, + InvalidRequestedRange, + MissingCoordinateDataset, + MissingValidCoordinateDataset, +) from hoss.projection_utilities import ( get_variable_crs, get_x_y_extents_from_geographic_points, @@ -59,43 +64,7 @@ def is_index_subset(message: Message) -> bool: ) -def get_override_projected_dimensions( - varinfo: VarInfoFromDmr, - override_variable_name: str, -) -> str: - """returns the x-y projection variable names that would - match the geo coordinate names - - """ - projection_variable_name = None - override_variable = varinfo.get_variable(override_variable_name) - if override_variable.is_latitude(): - projection_variable_name = 'projected_y' - elif override_variable.is_longitude(): - projection_variable_name = 'projected_x' - return projection_variable_name - - -def get_override_dimensions( - varinfo: VarInfoFromDmr, - required_variables: Set[str], -) -> set[str]: - """Determine the dimensions that need to be "pre-fetched" from OPeNDAP - If dimensions are not available, get variables that can be - used to generate dimensions - """ - # check if coordinate variables are provided - # this should be be a configurable in hoss_config.json - try: - override_dimensions = varinfo.get_references_for_attribute( - required_variables, 'coordinates' - ) - return override_dimensions - except AttributeError: - return set() - - -def prefetch_dimension_variables( +def get_prefetch_variables( opendap_url: str, varinfo: VarInfoFromDmr, required_variables: Set[str], @@ -109,36 +78,77 @@ def prefetch_dimension_variables( spatial and temporal dimensions, but to support generic dimension subsets, all required dimensions must be prefetched, along with any associated bounds variables referred to via the "bounds" metadata - attribute. + attribute. In cases where dimension variables do not exist, coordinate + variables will be prefetched and used to calculate dimension-scale values """ required_dimensions = varinfo.get_required_dimensions(required_variables) - if len(required_dimensions) == 0: - required_dimensions = get_override_dimensions(varinfo, required_variables) - logger.info('coordinates: ' f'{required_dimensions}') - + if not required_dimensions: + coordinate_variables = get_coordinate_variables(varinfo, required_variables) + logger.info('coordinates: ' f'{coordinate_variables}') + required_dimensions = set(coordinate_variables) + logger.info('required_dimensions: ' f'{required_dimensions}') bounds = varinfo.get_references_for_attribute(required_dimensions, 'bounds') - logger.info('bounds: ' f'{bounds}') required_dimensions.update(bounds) logger.info( 'Variables being retrieved in prefetch request: ' f'{format_variable_set_string(required_dimensions)}' ) + required_dimensions_nc4 = get_opendap_nc4( opendap_url, required_dimensions, output_dir, logger, access_token, config ) + # Create bounds variables if necessary. add_bounds_variables(required_dimensions_nc4, required_dimensions, varinfo, logger) return required_dimensions_nc4 -def is_variable_one_dimensional( - prefetch_dataset: Dataset, dimension_variable: VariableFromDmr -) -> bool: - """Check if a dimension variable is 1D""" - dimensions_array = prefetch_dataset[dimension_variable.full_name_path][:] - return dimensions_array.ndim == 1 +def get_override_projected_dimensions( + varinfo: VarInfoFromDmr, + override_variable_name: str, +) -> str | None: + """returns the x-y projection variable names that would + match the geo coordinate names. The `latitude` coordinate + variable name gets converted to 'projected_y' dimension scale + and the `longitude` coordinate variable name gets converted to + 'projected_x' + + """ + override_variable = varinfo.get_variable(override_variable_name) + if override_variable is not None: + if override_variable.is_latitude(): + projected_dimension_name = 'projected_y' + elif override_variable.is_longitude(): + projected_dimension_name = 'projected_x' + else: + projected_dimension_name = None + return projected_dimension_name + + +def get_coordinate_variables( + varinfo: VarInfoFromDmr, + requested_variables: Set[str], +) -> list[str]: + """This method returns coordinate variables that are referenced + in the variables requested. + """ + + try: + coordinate_variables_set = varinfo.get_references_for_attribute( + requested_variables, 'coordinates' + ) + coordinate_variables = [] + for coordinate in coordinate_variables_set: + if varinfo.get_variable(coordinate).is_latitude(): + coordinate_variables.insert(0, coordinate) + elif varinfo.get_variable(coordinate).is_longitude(): + coordinate_variables.insert(1, coordinate) + + return coordinate_variables + except AttributeError: + return set() def update_dimension_variables( @@ -146,27 +156,27 @@ def update_dimension_variables( required_dimensions: Set[str], varinfo: VarInfoFromDmr, ) -> Dict[str, ndarray]: - """Augment a NetCDF4 file with artificial 1D dimensions variable for each - 2D dimension variable" + """Generate artificial 1D dimensions variable for each + 2D dimension or coordinate variable For each dimension variable: (1) Check if the dimension variable is 1D. - (2) If it is not 1D and is 2D create a dimensions array from - within the `write_1D_dimensions` - function. - (3) Then write the 1D dimensions variable to the NetCDF4 URL. + (2) If it is not 1D and is 2D get the dimension sizes + (3) Get the corner points from the coordinate variables + (4) Get the x-y max-min values + (5) Generate the x-y dimscale array and return to the calling method """ for dimension_name in required_dimensions: dimension_variable = varinfo.get_variable(dimension_name) - if not is_variable_one_dimensional(prefetch_dataset, dimension_variable): + if prefetch_dataset[dimension_variable.full_name_path][:].ndim > 1: col_size = prefetch_dataset[dimension_variable.full_name_path][:].shape[0] row_size = prefetch_dataset[dimension_variable.full_name_path][:].shape[1] - crs = get_variable_crs(dimension_name, varinfo) + crs = get_variable_crs(dimension_name, varinfo) - geo_grid_corners = get_geo_grid_corners( - prefetch_dataset, required_dimensions, varinfo - ) + geo_grid_corners = get_geo_grid_corners( + prefetch_dataset, required_dimensions, varinfo + ) x_y_extents = get_x_y_extents_from_geographic_points(geo_grid_corners, crs) @@ -181,7 +191,7 @@ def update_dimension_variables( # create the xy dim scales x_dim = np.arange(x_min, x_max, x_resolution) - # ascending versus descending..should be based on the coordinate grid + # The origin is the top left. Y values are in decreasing order. y_dim = np.arange(y_max, y_min, -y_resolution) return {'projected_y': y_dim, 'projected_x': x_dim} @@ -193,10 +203,11 @@ def get_geo_grid_corners( ) -> list[Tuple[float]]: """ This method is used to return the lat lon corners from a 2D - coordinate dataset. This does a check for values below -180 - which could be fill values. Does not check if there are fill - values in the corner points to go down to the next row and col - + coordinate dataset. It gets the row and column of the latitude and longitude + arrays to get the corner points. This does a check for values below -180 + which could be fill values. This method does not check if there + are fill values in the corner points to go down to the next row and col + The fill values in the corner points still needs to be addressed. """ for dimension_name in required_dimensions: dimension_variable = varinfo.get_variable(dimension_name) @@ -205,6 +216,11 @@ def get_geo_grid_corners( elif dimension_variable.is_longitude(): lon_arr = prefetch_dataset[dimension_variable.full_name_path][:] + if not lat_arr.size: + raise MissingCoordinateDataset('latitude') + if not lon_arr.size: + raise MissingCoordinateDataset('longitude') + # skip fill values when calculating min values # topleft = minlon, maxlat # bottomright = maxlon, minlat @@ -212,6 +228,8 @@ def get_geo_grid_corners( top_left_col_idx = 0 lon_row = lon_arr[top_left_row_idx, :] lon_row_valid_indices = np.where(lon_row >= -180.0)[0] + if not lon_row_valid_indices.size: + raise MissingValidCoordinateDataset('longitude') top_left_col_idx = lon_row_valid_indices[lon_row[lon_row_valid_indices].argmin()] minlon = lon_row[top_left_col_idx] top_right_col_idx = lon_row_valid_indices[lon_row[lon_row_valid_indices].argmax()] @@ -219,6 +237,8 @@ def get_geo_grid_corners( lat_col = lat_arr[:, top_right_col_idx] lat_col_valid_indices = np.where(lat_col >= -180.0)[0] + if not lat_col_valid_indices.size: + raise MissingValidCoordinateDataset('latitude') bottom_right_row_idx = lat_col_valid_indices[ lat_col[lat_col_valid_indices].argmin() ] @@ -560,40 +580,23 @@ def add_index_range( """ variable = varinfo.get_variable(variable_name) range_strings = [] - if variable.dimensions == []: - override_dimensions = get_override_dimensions(varinfo, [variable_name]) - if len(override_dimensions) > 0: - for override in reversed(list(override_dimensions)): - dimension = get_override_projected_dimensions(varinfo, override) - dimension_range = index_ranges.get(dimension) - if ( - dimension_range is not None - and dimension_range[0] <= dimension_range[1] - ): - range_strings.append(f'[{dimension_range[0]}:{dimension_range[1]}]') - else: - range_strings.append('[]') + if variable.dimensions: + range_strings = get_range_strings(variable.dimensions, index_ranges) + else: + coordinate_variables = get_coordinate_variables(varinfo, [variable_name]) + if coordinate_variables: + dimensions = [] + for coordinate in coordinate_variables: + dimensions.append( + get_override_projected_dimensions(varinfo, coordinate) + ) + range_strings = get_range_strings(dimensions, index_ranges) else: # if the override is the variable override = get_override_projected_dimensions(varinfo, variable_name) - if override is not None and override in index_ranges.keys(): - for dimension in reversed(index_ranges.keys()): - dimension_range = index_ranges.get(dimension) - if ( - dimension_range is not None - and dimension_range[0] <= dimension_range[1] - ): - range_strings.append( - f'[{dimension_range[0]}:{dimension_range[1]}]' - ) - else: - range_strings.append('[]') - else: - for dimension in variable.dimensions: - dimension_range = index_ranges.get(dimension) - - if dimension_range is not None and dimension_range[0] <= dimension_range[1]: - range_strings.append(f'[{dimension_range[0]}:{dimension_range[1]}]') + dimensions = ['projected_y', 'projected_x'] + if override is not None and override in dimensions: + range_strings = get_range_strings(dimensions, index_ranges) else: range_strings.append('[]') @@ -604,6 +607,24 @@ def add_index_range( return f'{variable_name}{indices_string}' +def get_range_strings( + variable_dimensions: list, + index_ranges: IndexRanges, +) -> list: + """Calculates the index ranges for each dimension of the variable + and returns the list of index ranges + """ + range_strings = [] + for dimension in variable_dimensions: + dimension_range = index_ranges.get(dimension) + if dimension_range is not None and dimension_range[0] <= dimension_range[1]: + range_strings.append(f'[{dimension_range[0]}:{dimension_range[1]}]') + else: + range_strings.append('[]') + + return range_strings + + def get_fill_slice(dimension: str, fill_ranges: IndexRanges) -> slice: """Check the dictionary of dimensions that need to be filled for the given dimension. If present, the minimum index will be greater than the @@ -713,6 +734,8 @@ def get_dimension_bounds( """ try: + # For pseudo-variables, `varinfo.get_variable` returns `None` and + # therefore has no `references` attribute. bounds = varinfo.get_variable(dimension_name).references.get('bounds') except AttributeError: bounds = None diff --git a/hoss/exceptions.py b/hoss/exceptions.py index 1cb1439..f92abc4 100644 --- a/hoss/exceptions.py +++ b/hoss/exceptions.py @@ -108,6 +108,36 @@ def __init__(self): ) +class MissingCoordinateDataset(CustomError): + """This exception is raised when HOSS tries to get latitude and longitude + datasets and they are missing or empty. These datasets are referred to + in the science variables with coordinate attributes. + + """ + + def __init__(self, referring_variable): + super().__init__( + 'MissingCoordinateDataset', + f'Coordinate: "{referring_variable}" is ' + 'not present in source granule file.', + ) + + +class MissingValidCoordinateDataset(CustomError): + """This exception is raised when HOSS tries to get latitude and longitude + datasets and they are missing or empty. These datasets are referred to + in the science variables with coordinate attributes. + + """ + + def __init__(self, referring_variable): + super().__init__( + 'MissingValidCoordinateDataset', + f'Coordinate: "{referring_variable}" is ' + 'not valid in source granule file.', + ) + + class UnsupportedShapeFileFormat(CustomError): """This exception is raised when the shape file included in the input Harmony message is not GeoJSON. diff --git a/hoss/projection_utilities.py b/hoss/projection_utilities.py index 8513b67..92f0a44 100644 --- a/hoss/projection_utilities.py +++ b/hoss/projection_utilities.py @@ -61,9 +61,10 @@ def get_variable_crs(variable: str, varinfo: VarInfoFromDmr) -> CRS: if grid_mapping_variable is None: # check for any overrides cf_attributes = varinfo.get_missing_variable_attributes(grid_mapping) - if len(cf_attributes) != 0: + if cf_attributes: crs = CRS.from_cf(cf_attributes) return crs + crs = CRS.from_cf(varinfo.get_variable(grid_mapping).attributes) except AttributeError as exception: @@ -71,6 +72,7 @@ def get_variable_crs(variable: str, varinfo: VarInfoFromDmr) -> CRS: else: raise MissingGridMappingMetadata(variable) + return crs diff --git a/hoss/spatial.py b/hoss/spatial.py index 8a88dfe..0e6737d 100644 --- a/hoss/spatial.py +++ b/hoss/spatial.py @@ -38,10 +38,10 @@ from hoss.dimension_utilities import ( IndexRange, IndexRanges, + get_coordinate_variables, get_dimension_bounds, get_dimension_extents, get_dimension_index_range, - get_override_dimensions, update_dimension_variables, ) from hoss.projection_utilities import ( @@ -104,7 +104,7 @@ def get_spatial_index_ranges( index_ranges[dimension] = get_geographic_index_range( dimension, varinfo, dimensions_file, bounding_box ) - elif len(projected_dimensions) > 0: + if len(projected_dimensions) > 0: for non_spatial_variable in non_spatial_variables: index_ranges.update( get_projected_x_y_index_ranges( @@ -116,9 +116,9 @@ def get_spatial_index_ranges( shape_file_path=shape_file_path, ) ) - else: - override_dimensions = get_override_dimensions(varinfo, required_variables) - if len(override_dimensions) > 0: + if len(geographic_dimensions) == 0 and len(projected_dimensions) == 0: + coordinate_variables = get_coordinate_variables(varinfo, required_variables) + if coordinate_variables: for non_spatial_variable in non_spatial_variables: index_ranges.update( get_projected_x_y_index_ranges( @@ -128,7 +128,7 @@ def get_spatial_index_ranges( index_ranges, bounding_box=bounding_box, shape_file_path=shape_file_path, - override_dimensions=override_dimensions, + override_dimensions=coordinate_variables, ) ) return index_ranges @@ -160,7 +160,7 @@ def get_projected_x_y_index_ranges( projected coordinate points. """ - if len(override_dimensions) == 0: + if not override_dimensions: projected_x, projected_y = get_projected_x_y_variables( varinfo, non_spatial_variable ) @@ -187,6 +187,7 @@ def get_projected_x_y_index_ranges( shape_file=shape_file_path, bounding_box=bounding_box, ) + x_bounds = get_dimension_bounds(projected_x, varinfo, dimensions_file) y_bounds = get_dimension_bounds(projected_y, varinfo, dimensions_file) x_index_ranges = get_dimension_index_range( diff --git a/hoss/subset.py b/hoss/subset.py index 80c8e4d..917b390 100644 --- a/hoss/subset.py +++ b/hoss/subset.py @@ -21,9 +21,9 @@ IndexRanges, add_index_range, get_fill_slice, + get_prefetch_variables, get_requested_index_ranges, is_index_subset, - prefetch_dimension_variables, ) from hoss.spatial import get_spatial_index_ranges from hoss.temporal import get_temporal_index_ranges @@ -92,7 +92,7 @@ def subset_granule( if request_is_index_subset: # Prefetch all dimension variables in full: - dimensions_path = prefetch_dimension_variables( + dimensions_path = get_prefetch_variables( opendap_url, varinfo, required_variables, @@ -127,14 +127,6 @@ def subset_granule( shape_file_path = get_request_shape_file( harmony_message, output_dir, logger, config ) - logger.info( - 'All required variables: ' - f'{format_variable_set_string(required_variables)}' - ', dimensions_path: ' - f'{dimensions_path}' - ', shapefilepath:' - f'{shape_file_path}' - ) index_ranges.update( get_spatial_index_ranges( @@ -145,9 +137,6 @@ def subset_granule( shape_file_path, ) ) - logger.info( - 'subset_granule - index_ranges:' f'{format_dictionary_string(index_ranges)}' - ) if harmony_message.temporal is not None: # Update `index_ranges` cache with ranges for temporal diff --git a/tests/unit/test_dimension_utilities.py b/tests/unit/test_dimension_utilities.py index 5ad7a21..9be9f07 100644 --- a/tests/unit/test_dimension_utilities.py +++ b/tests/unit/test_dimension_utilities.py @@ -23,12 +23,12 @@ get_dimension_indices_from_bounds, get_dimension_indices_from_values, get_fill_slice, + get_prefetch_variables, get_requested_index_ranges, is_almost_in, is_dimension_ascending, is_index_subset, needs_bounds, - prefetch_dimension_variables, write_bounds, ) from hoss.exceptions import InvalidNamedDimension, InvalidRequestedRange @@ -328,7 +328,7 @@ def test_get_fill_slice(self): @patch('hoss.dimension_utilities.add_bounds_variables') @patch('hoss.dimension_utilities.get_opendap_nc4') - def test_prefetch_dimension_variables( + def test_get_prefetch_variables( self, mock_get_opendap_nc4, mock_add_bounds_variables ): """Ensure that when a list of required variables is specified, a @@ -349,7 +349,7 @@ def test_prefetch_dimension_variables( required_dimensions = {'/latitude', '/longitude', '/time'} self.assertEqual( - prefetch_dimension_variables( + get_prefetch_variables( url, self.varinfo, required_variables, @@ -569,7 +569,7 @@ def test_prefetch_dimensions_with_bounds(self, mock_get_opendap_nc4): } self.assertEqual( - prefetch_dimension_variables( + get_prefetch_variables( url, self.varinfo_with_bounds, required_variables, diff --git a/tests/unit/test_subset.py b/tests/unit/test_subset.py index abbb5f1..7c6e979 100644 --- a/tests/unit/test_subset.py +++ b/tests/unit/test_subset.py @@ -62,7 +62,7 @@ def tearDown(self): @patch('hoss.subset.get_requested_index_ranges') @patch('hoss.subset.get_temporal_index_ranges') @patch('hoss.subset.get_spatial_index_ranges') - @patch('hoss.subset.prefetch_dimension_variables') + @patch('hoss.subset.get_prefetch_variables') @patch('hoss.subset.get_varinfo') def test_subset_granule_not_geo( self, @@ -126,7 +126,7 @@ def test_subset_granule_not_geo( @patch('hoss.subset.get_requested_index_ranges') @patch('hoss.subset.get_temporal_index_ranges') @patch('hoss.subset.get_spatial_index_ranges') - @patch('hoss.subset.prefetch_dimension_variables') + @patch('hoss.subset.get_prefetch_variables') @patch('hoss.subset.get_varinfo') def test_subset_granule_geo( self, @@ -216,7 +216,7 @@ def test_subset_granule_geo( @patch('hoss.subset.get_requested_index_ranges') @patch('hoss.subset.get_temporal_index_ranges') @patch('hoss.subset.get_spatial_index_ranges') - @patch('hoss.subset.prefetch_dimension_variables') + @patch('hoss.subset.get_prefetch_variables') @patch('hoss.subset.get_varinfo') def test_subset_non_geo_no_variables( self, @@ -290,7 +290,7 @@ def test_subset_non_geo_no_variables( @patch('hoss.subset.get_requested_index_ranges') @patch('hoss.subset.get_temporal_index_ranges') @patch('hoss.subset.get_spatial_index_ranges') - @patch('hoss.subset.prefetch_dimension_variables') + @patch('hoss.subset.get_prefetch_variables') @patch('hoss.subset.get_varinfo') def test_subset_geo_no_variables( self, @@ -404,7 +404,7 @@ def test_subset_geo_no_variables( @patch('hoss.subset.get_requested_index_ranges') @patch('hoss.subset.get_temporal_index_ranges') @patch('hoss.subset.get_spatial_index_ranges') - @patch('hoss.subset.prefetch_dimension_variables') + @patch('hoss.subset.get_prefetch_variables') @patch('hoss.subset.get_varinfo') def test_subset_non_variable_dimensions( self, @@ -537,7 +537,7 @@ def test_subset_non_variable_dimensions( @patch('hoss.subset.get_requested_index_ranges') @patch('hoss.subset.get_temporal_index_ranges') @patch('hoss.subset.get_spatial_index_ranges') - @patch('hoss.subset.prefetch_dimension_variables') + @patch('hoss.subset.get_prefetch_variables') @patch('hoss.subset.get_varinfo') def test_subset_bounds_reference( self, @@ -638,7 +638,7 @@ def test_subset_bounds_reference( @patch('hoss.subset.get_requested_index_ranges') @patch('hoss.subset.get_temporal_index_ranges') @patch('hoss.subset.get_spatial_index_ranges') - @patch('hoss.subset.prefetch_dimension_variables') + @patch('hoss.subset.get_prefetch_variables') @patch('hoss.subset.get_varinfo') def test_subset_temporal( self, @@ -742,7 +742,7 @@ def test_subset_temporal( @patch('hoss.subset.get_requested_index_ranges') @patch('hoss.subset.get_temporal_index_ranges') @patch('hoss.subset.get_spatial_index_ranges') - @patch('hoss.subset.prefetch_dimension_variables') + @patch('hoss.subset.get_prefetch_variables') @patch('hoss.subset.get_varinfo') def test_subset_geo_temporal( self, @@ -860,7 +860,7 @@ def test_subset_geo_temporal( @patch('hoss.subset.get_temporal_index_ranges') @patch('hoss.subset.get_spatial_index_ranges') @patch('hoss.subset.get_request_shape_file') - @patch('hoss.subset.prefetch_dimension_variables') + @patch('hoss.subset.get_prefetch_variables') @patch('hoss.subset.get_varinfo') def test_subset_granule_shape( self, @@ -971,7 +971,7 @@ def test_subset_granule_shape( @patch('hoss.subset.get_temporal_index_ranges') @patch('hoss.subset.get_spatial_index_ranges') @patch('hoss.subset.get_request_shape_file') - @patch('hoss.subset.prefetch_dimension_variables') + @patch('hoss.subset.get_prefetch_variables') @patch('hoss.subset.get_varinfo') def test_subset_granule_shape_and_bbox( self, @@ -1083,7 +1083,7 @@ def test_subset_granule_shape_and_bbox( @patch('hoss.subset.get_requested_index_ranges') @patch('hoss.subset.get_temporal_index_ranges') @patch('hoss.subset.get_spatial_index_ranges') - @patch('hoss.subset.prefetch_dimension_variables') + @patch('hoss.subset.get_prefetch_variables') @patch('hoss.subset.get_varinfo') def test_subset_granule_geo_named( self, From 53f1660cd752267404385db82fc42de42704d934 Mon Sep 17 00:00:00 2001 From: sudhamurthy Date: Thu, 26 Sep 2024 12:28:17 -0400 Subject: [PATCH 10/41] DAS-2232 - fixed get_variable_crs and a few minor corrections --- hoss/dimension_utilities.py | 3 +-- hoss/projection_utilities.py | 12 +++++++----- hoss/spatial.py | 6 +++--- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/hoss/dimension_utilities.py b/hoss/dimension_utilities.py index 11e8cdf..bef8470 100644 --- a/hoss/dimension_utilities.py +++ b/hoss/dimension_utilities.py @@ -85,9 +85,8 @@ def get_prefetch_variables( required_dimensions = varinfo.get_required_dimensions(required_variables) if not required_dimensions: coordinate_variables = get_coordinate_variables(varinfo, required_variables) - logger.info('coordinates: ' f'{coordinate_variables}') required_dimensions = set(coordinate_variables) - logger.info('required_dimensions: ' f'{required_dimensions}') + bounds = varinfo.get_references_for_attribute(required_dimensions, 'bounds') required_dimensions.update(bounds) diff --git a/hoss/projection_utilities.py b/hoss/projection_utilities.py index 92f0a44..d84c64d 100644 --- a/hoss/projection_utilities.py +++ b/hoss/projection_utilities.py @@ -58,14 +58,16 @@ def get_variable_crs(variable: str, varinfo: VarInfoFromDmr) -> CRS: if grid_mapping is not None: try: grid_mapping_variable = varinfo.get_variable(grid_mapping) - if grid_mapping_variable is None: + if grid_mapping_variable is not None: + cf_attributes = grid_mapping_variable.attributes + else: # check for any overrides cf_attributes = varinfo.get_missing_variable_attributes(grid_mapping) - if cf_attributes: - crs = CRS.from_cf(cf_attributes) - return crs - crs = CRS.from_cf(varinfo.get_variable(grid_mapping).attributes) + if cf_attributes: + crs = CRS.from_cf(cf_attributes) + else: + raise MissingGridMappingVariable(grid_mapping, variable) except AttributeError as exception: raise MissingGridMappingVariable(grid_mapping, variable) from exception diff --git a/hoss/spatial.py b/hoss/spatial.py index 0e6737d..02ea903 100644 --- a/hoss/spatial.py +++ b/hoss/spatial.py @@ -93,7 +93,7 @@ def get_spatial_index_ranges( ) with Dataset(dimensions_path, 'r') as dimensions_file: - if len(geographic_dimensions) > 0: + if geographic_dimensions: # If there is no bounding box, but there is a shape file, calculate # a bounding box to encapsulate the GeoJSON shape: if bounding_box is None and shape_file_path is not None: @@ -104,7 +104,7 @@ def get_spatial_index_ranges( index_ranges[dimension] = get_geographic_index_range( dimension, varinfo, dimensions_file, bounding_box ) - if len(projected_dimensions) > 0: + if projected_dimensions: for non_spatial_variable in non_spatial_variables: index_ranges.update( get_projected_x_y_index_ranges( @@ -116,7 +116,7 @@ def get_spatial_index_ranges( shape_file_path=shape_file_path, ) ) - if len(geographic_dimensions) == 0 and len(projected_dimensions) == 0: + if (not geographic_dimensions) and (not projected_dimensions): coordinate_variables = get_coordinate_variables(varinfo, required_variables) if coordinate_variables: for non_spatial_variable in non_spatial_variables: From f9f5e8b949dbf7718495ef9fe627b9c92ec8e7c2 Mon Sep 17 00:00:00 2001 From: sudhamurthy Date: Thu, 26 Sep 2024 13:30:55 -0400 Subject: [PATCH 11/41] DAS-2232 - comments updated for the get_spatial_index_ranges method --- hoss/spatial.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/hoss/spatial.py b/hoss/spatial.py index 02ea903..ce4961e 100644 --- a/hoss/spatial.py +++ b/hoss/spatial.py @@ -80,6 +80,13 @@ def get_spatial_index_ranges( around the exterior of the user-defined GeoJSON shape, to ensure the correct extents are derived. + For projected grids which do not follow CF standards, the projected + dimension scales are computed based on the values in the coordinate + datasets if they are available. The geocorners are obtained from the + coordinate datasets and converted to projected meters based on the crs + of the product. The dimension scales are then computed based on the + grid size and grid resolution + """ bounding_box = get_harmony_message_bbox(harmony_message) index_ranges = {} From f836ee690d2433c52b2cb6fb8f0cac3b2d7f0e06 Mon Sep 17 00:00:00 2001 From: sudhamurthy Date: Thu, 26 Sep 2024 14:04:21 -0400 Subject: [PATCH 12/41] DAS-2232 simplified get_spatial_index_ranges method --- hoss/spatial.py | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/hoss/spatial.py b/hoss/spatial.py index ce4961e..f9027db 100644 --- a/hoss/spatial.py +++ b/hoss/spatial.py @@ -90,11 +90,15 @@ def get_spatial_index_ranges( """ bounding_box = get_harmony_message_bbox(harmony_message) index_ranges = {} + coordinate_variables = [] geographic_dimensions = varinfo.get_geographic_spatial_dimensions( required_variables ) projected_dimensions = varinfo.get_projected_spatial_dimensions(required_variables) + if (not geographic_dimensions) and (not projected_dimensions): + coordinate_variables = get_coordinate_variables(varinfo, required_variables) + non_spatial_variables = required_variables.difference( varinfo.get_spatial_dimensions(required_variables) ) @@ -111,7 +115,7 @@ def get_spatial_index_ranges( index_ranges[dimension] = get_geographic_index_range( dimension, varinfo, dimensions_file, bounding_box ) - if projected_dimensions: + if projected_dimensions or coordinate_variables: for non_spatial_variable in non_spatial_variables: index_ranges.update( get_projected_x_y_index_ranges( @@ -121,23 +125,9 @@ def get_spatial_index_ranges( index_ranges, bounding_box=bounding_box, shape_file_path=shape_file_path, + override_dimensions=coordinate_variables, ) ) - if (not geographic_dimensions) and (not projected_dimensions): - coordinate_variables = get_coordinate_variables(varinfo, required_variables) - if coordinate_variables: - for non_spatial_variable in non_spatial_variables: - index_ranges.update( - get_projected_x_y_index_ranges( - non_spatial_variable, - varinfo, - dimensions_file, - index_ranges, - bounding_box=bounding_box, - shape_file_path=shape_file_path, - override_dimensions=coordinate_variables, - ) - ) return index_ranges From c35c8ee5e40f934b8b434d5008c0221c63491e06 Mon Sep 17 00:00:00 2001 From: sudhamurthy Date: Thu, 26 Sep 2024 15:26:30 -0400 Subject: [PATCH 13/41] DAS-2232 - changed override to coordinates and dimension_file to dimension_datasets for clarity --- hoss/dimension_utilities.py | 23 +++++++++++------------ hoss/spatial.py | 31 +++++++++++++++++-------------- 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/hoss/dimension_utilities.py b/hoss/dimension_utilities.py index bef8470..b16da4a 100644 --- a/hoss/dimension_utilities.py +++ b/hoss/dimension_utilities.py @@ -131,7 +131,8 @@ def get_coordinate_variables( requested_variables: Set[str], ) -> list[str]: """This method returns coordinate variables that are referenced - in the variables requested. + in the variables requested. It returns it in a specific order + [latitude, longitude] """ try: @@ -152,7 +153,7 @@ def get_coordinate_variables( def update_dimension_variables( prefetch_dataset: Dataset, - required_dimensions: Set[str], + coordinates: Set[str], varinfo: VarInfoFromDmr, ) -> Dict[str, ndarray]: """Generate artificial 1D dimensions variable for each @@ -166,16 +167,14 @@ def update_dimension_variables( (5) Generate the x-y dimscale array and return to the calling method """ - for dimension_name in required_dimensions: - dimension_variable = varinfo.get_variable(dimension_name) - if prefetch_dataset[dimension_variable.full_name_path][:].ndim > 1: - col_size = prefetch_dataset[dimension_variable.full_name_path][:].shape[0] - row_size = prefetch_dataset[dimension_variable.full_name_path][:].shape[1] - crs = get_variable_crs(dimension_name, varinfo) - - geo_grid_corners = get_geo_grid_corners( - prefetch_dataset, required_dimensions, varinfo - ) + for coordinate_name in coordinates: + coordinate_variable = varinfo.get_variable(coordinate_name) + if prefetch_dataset[coordinate_variable.full_name_path][:].ndim > 1: + col_size = prefetch_dataset[coordinate_variable.full_name_path][:].shape[0] + row_size = prefetch_dataset[coordinate_variable.full_name_path][:].shape[1] + crs = get_variable_crs(coordinate_name, varinfo) + + geo_grid_corners = get_geo_grid_corners(prefetch_dataset, coordinates, varinfo) x_y_extents = get_x_y_extents_from_geographic_points(geo_grid_corners, crs) diff --git a/hoss/spatial.py b/hoss/spatial.py index f9027db..899fd22 100644 --- a/hoss/spatial.py +++ b/hoss/spatial.py @@ -125,7 +125,7 @@ def get_spatial_index_ranges( index_ranges, bounding_box=bounding_box, shape_file_path=shape_file_path, - override_dimensions=coordinate_variables, + coordinates=coordinate_variables, ) ) return index_ranges @@ -134,11 +134,11 @@ def get_spatial_index_ranges( def get_projected_x_y_index_ranges( non_spatial_variable: str, varinfo: VarInfoFromDmr, - dimensions_file: Dataset, + dimension_datasets: Dataset, index_ranges: IndexRanges, bounding_box: BBox = None, shape_file_path: str = None, - override_dimensions: Set[str] = set(), + coordinates: Set[str] = set(), ) -> IndexRanges: """This function returns a dictionary containing the minimum and maximum index ranges for a pair of projection x and y coordinates, e.g.: @@ -157,19 +157,21 @@ def get_projected_x_y_index_ranges( projected coordinate points. """ - if not override_dimensions: + if not coordinates: projected_x, projected_y = get_projected_x_y_variables( varinfo, non_spatial_variable ) + else: projected_x = 'projected_x' projected_y = 'projected_y' - override_dimensions_file = update_dimension_variables( - dimensions_file, - override_dimensions, + override_dimension_datasets = update_dimension_variables( + dimension_datasets, + coordinates, varinfo, ) - dimensions_file = override_dimensions_file + dimension_datasets = override_dimension_datasets + if ( projected_x is not None and projected_y is not None @@ -178,23 +180,24 @@ def get_projected_x_y_index_ranges( crs = get_variable_crs(non_spatial_variable, varinfo) x_y_extents = get_projected_x_y_extents( - dimensions_file[projected_x][:], - dimensions_file[projected_y][:], + dimension_datasets[projected_x][:], + dimension_datasets[projected_y][:], crs, shape_file=shape_file_path, bounding_box=bounding_box, ) - x_bounds = get_dimension_bounds(projected_x, varinfo, dimensions_file) - y_bounds = get_dimension_bounds(projected_y, varinfo, dimensions_file) + x_bounds = get_dimension_bounds(projected_x, varinfo, dimension_datasets) + y_bounds = get_dimension_bounds(projected_y, varinfo, dimension_datasets) + x_index_ranges = get_dimension_index_range( - dimensions_file[projected_x][:], + dimension_datasets[projected_x][:], x_y_extents['x_min'], x_y_extents['x_max'], bounds_values=x_bounds, ) y_index_ranges = get_dimension_index_range( - dimensions_file[projected_y][:], + dimension_datasets[projected_y][:], x_y_extents['y_min'], x_y_extents['y_max'], bounds_values=y_bounds, From ffe035abd254f7c9ec3c4b62edff44e416eccae1 Mon Sep 17 00:00:00 2001 From: sudhamurthy Date: Thu, 26 Sep 2024 18:55:48 -0400 Subject: [PATCH 14/41] DAS-2232 - updates to CHANGELOG.md --- CHANGELOG.md | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d24dc19..f977834 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,18 +1,21 @@ ## v1.1.0 ### 2024-09-10 -This version of HOSS provides support for products that do not comply with CF standards like SMAP L3 -New methods added to retrieve dimension scales from coordinate attributes and grid mappings, using -overrides specified in the hoss_config.json configuration file. `get_coordinate_variables' gets coordinate -datasets when the dimension scales are not present in the source file. The prefetch gets the coordinate -datasets during prefetch when the dimension scales are not present. `is_variable_one_dimensional' function -checks the dimensionality of the coordinate datasets. `update_dimension_variables' gets a row and column -from the 2D datasets to 1D and uses the crs attribute to get the projection of the granule to convert the -lat/lon array to projected x-y dimension scales. `get_override_projected_dimensions` provides the projected -dimension scale names after the conversion. The `get_variable_crs' also updated when the -grid mapping variable does not exist in the granule and an override is provided in an updated hoss_config.json - -`get_override_projection_dimensions` +This version of HOSS provides support for products that do not comply with CF standards like SMAP L3. +New methods are added to retrieve dimension scales from coordinate attributes and grid mappings, using +overrides specified in the hoss_config.json configuration file. +- `get_coordinate_variables` gets coordinate datasets when the dimension scales are not present in + the source file. The prefetch gets the coordinate datasets during prefetch when the dimension + scales are not present. +- `update_dimension_variables` function checks the dimensionality of the coordinate datasets. + It then gets a row and column from the 2D datasets to 1D and uses the crs attribute to get + the projection of the granule to convert the lat/lon array to projected x-y dimension scales. +- `get_override_projected_dimensions` provides the projected dimension scale names after the + conversion. +- `get_variable_crs` method is also updated to handle cases where the grid mapping variable + does not exist in the granule and an override is provided in an updated + hoss_config.json + ## v1.0.5 ### 2024-08-19 From 9c9f1908c770b15ff6535e43723230a892e7f7d1 Mon Sep 17 00:00:00 2001 From: sudhamurthy Date: Fri, 27 Sep 2024 14:00:23 -0400 Subject: [PATCH 15/41] DAS-2232 - PR feedback update - to detrmine if latitude is ascending and few other changes --- hoss/dimension_utilities.py | 105 ++++++++++++++++++++++++++---------- 1 file changed, 78 insertions(+), 27 deletions(-) diff --git a/hoss/dimension_utilities.py b/hoss/dimension_utilities.py index b16da4a..bb12472 100644 --- a/hoss/dimension_utilities.py +++ b/hoss/dimension_utilities.py @@ -135,20 +135,20 @@ def get_coordinate_variables( [latitude, longitude] """ - try: - coordinate_variables_set = varinfo.get_references_for_attribute( - requested_variables, 'coordinates' - ) - coordinate_variables = [] - for coordinate in coordinate_variables_set: - if varinfo.get_variable(coordinate).is_latitude(): - coordinate_variables.insert(0, coordinate) - elif varinfo.get_variable(coordinate).is_longitude(): - coordinate_variables.insert(1, coordinate) - - return coordinate_variables - except AttributeError: - return set() + # try: + coordinate_variables_set = varinfo.get_references_for_attribute( + requested_variables, 'coordinates' + ) + coordinate_variables = [] + for coordinate in coordinate_variables_set: + if varinfo.get_variable(coordinate).is_latitude(): + coordinate_variables.insert(0, coordinate) + elif varinfo.get_variable(coordinate).is_longitude(): + coordinate_variables.insert(1, coordinate) + + return coordinate_variables + # except AttributeError: + # return set() def update_dimension_variables( @@ -174,7 +174,7 @@ def update_dimension_variables( row_size = prefetch_dataset[coordinate_variable.full_name_path][:].shape[1] crs = get_variable_crs(coordinate_name, varinfo) - geo_grid_corners = get_geo_grid_corners(prefetch_dataset, coordinates, varinfo) + geo_grid_corners = get_geo_grid_corners(prefetch_dataset, coordinates, varinfo) x_y_extents = get_x_y_extents_from_geographic_points(geo_grid_corners, crs) @@ -189,14 +189,53 @@ def update_dimension_variables( # create the xy dim scales x_dim = np.arange(x_min, x_max, x_resolution) - # The origin is the top left. Y values are in decreasing order. - y_dim = np.arange(y_max, y_min, -y_resolution) + # The origin is usually the top left and Y values are in decreasing order. + if is_latitude_ascending(prefetch_dataset, coordinates, varinfo): + y_dim = np.arange(y_max, y_min, y_resolution) + else: + y_dim = np.arange(y_max, y_min, -y_resolution) + return {'projected_y': y_dim, 'projected_x': x_dim} +def is_latitude_ascending( + prefetch_dataset: Dataset, + coordinates: Set[str], + varinfo: VarInfoFromDmr, +) -> bool: + """ + Checks if the latitude cooordinate datasets have values + that are ascending + """ + lat_arr, lon_arr = get_lat_lon_arrays(prefetch_dataset, coordinates, varinfo) + lat_col = lat_arr[:, 0] + return is_dimension_ascending(lat_col) + + +def get_lat_lon_arrays( + prefetch_dataset: Dataset, + coordinates: Set[str], + varinfo: VarInfoFromDmr, +) -> Tuple[ndarray, ndarray]: + """ + This method is used to return the lat lon arrays from a 2D + coordinate dataset. + """ + lat_arr = [] + lon_arr = [] + for coordinate in coordinates: + coordinate_variable = varinfo.get_variable(coordinate) + if coordinate_variable.is_latitude(): + lat_arr = prefetch_dataset[coordinate_variable.full_name_path][:] + elif coordinate_variable.is_longitude(): + lon_arr = prefetch_dataset[coordinate_variable.full_name_path][:] + + return lat_arr, lon_arr + + def get_geo_grid_corners( prefetch_dataset: Dataset, - required_dimensions: Set[str], + coordinates: Set[str], varinfo: VarInfoFromDmr, ) -> list[Tuple[float]]: """ @@ -207,13 +246,7 @@ def get_geo_grid_corners( are fill values in the corner points to go down to the next row and col The fill values in the corner points still needs to be addressed. """ - for dimension_name in required_dimensions: - dimension_variable = varinfo.get_variable(dimension_name) - if dimension_variable.is_latitude(): - lat_arr = prefetch_dataset[dimension_variable.full_name_path][:] - elif dimension_variable.is_longitude(): - lon_arr = prefetch_dataset[dimension_variable.full_name_path][:] - + lat_arr, lon_arr = get_lat_lon_arrays(prefetch_dataset, coordinates, varinfo) if not lat_arr.size: raise MissingCoordinateDataset('latitude') if not lon_arr.size: @@ -224,23 +257,41 @@ def get_geo_grid_corners( # bottomright = maxlon, minlat top_left_row_idx = 0 top_left_col_idx = 0 + + # get the first row from the longitude dataset lon_row = lon_arr[top_left_row_idx, :] - lon_row_valid_indices = np.where(lon_row >= -180.0)[0] + lon_row_valid_indices = np.where((lon_row >= -180.0) & (lon_row <= 180.0))[0] + + # if the first row does not have valid indices, + # should go down to the next row. We throw an exception + # for now till that gets addressed if not lon_row_valid_indices.size: raise MissingValidCoordinateDataset('longitude') + + # get the index of the minimum longitude after checking for invalid entries top_left_col_idx = lon_row_valid_indices[lon_row[lon_row_valid_indices].argmin()] minlon = lon_row[top_left_col_idx] + + # get the index of the maximum longitude after checking for invalid entries top_right_col_idx = lon_row_valid_indices[lon_row[lon_row_valid_indices].argmax()] maxlon = lon_row[top_right_col_idx] + # get the last valid longitude column to get the latitude array lat_col = lat_arr[:, top_right_col_idx] - lat_col_valid_indices = np.where(lat_col >= -180.0)[0] + lat_col_valid_indices = np.where((lat_col >= -90.0) & (lat_col <= 90.0))[0] + + # if the longitude values are invalid, should check the other columns + # We throw an exception for now till that gets addressed if not lat_col_valid_indices.size: raise MissingValidCoordinateDataset('latitude') + + # get the index of minimum latitude after checking for valid values bottom_right_row_idx = lat_col_valid_indices[ lat_col[lat_col_valid_indices].argmin() ] minlat = lat_col[bottom_right_row_idx] + + # get the index of maximum latitude after checking for valid values top_right_row_idx = lat_col_valid_indices[lat_col[lat_col_valid_indices].argmax()] maxlat = lat_col[top_right_row_idx] From 7eda980775f20bc88419852c1e8c001ac612766d Mon Sep 17 00:00:00 2001 From: sudhamurthy Date: Tue, 1 Oct 2024 02:25:29 -0400 Subject: [PATCH 16/41] DAS-2232 - added get_x_y_index_ranges_from_coordinates as a separate method --- hoss/dimension_utilities.py | 48 ++++++++---- hoss/exceptions.py | 15 ++++ hoss/spatial.py | 142 +++++++++++++++++++++++++++--------- hoss/subset.py | 1 - 4 files changed, 156 insertions(+), 50 deletions(-) diff --git a/hoss/dimension_utilities.py b/hoss/dimension_utilities.py index bb12472..f4ed02c 100644 --- a/hoss/dimension_utilities.py +++ b/hoss/dimension_utilities.py @@ -21,16 +21,17 @@ from netCDF4 import Dataset from numpy import ndarray from numpy.ma.core import MaskedArray +from pyproj import CRS from varinfo import VariableFromDmr, VarInfoFromDmr from hoss.exceptions import ( InvalidNamedDimension, InvalidRequestedRange, + IrregularCoordinateDatasets, MissingCoordinateDataset, MissingValidCoordinateDataset, ) from hoss.projection_utilities import ( - get_variable_crs, get_x_y_extents_from_geographic_points, ) from hoss.utilities import ( @@ -83,12 +84,13 @@ def get_prefetch_variables( """ required_dimensions = varinfo.get_required_dimensions(required_variables) - if not required_dimensions: + if required_dimensions: + bounds = varinfo.get_references_for_attribute(required_dimensions, 'bounds') + required_dimensions.update(bounds) + else: coordinate_variables = get_coordinate_variables(varinfo, required_variables) - required_dimensions = set(coordinate_variables) - - bounds = varinfo.get_references_for_attribute(required_dimensions, 'bounds') - required_dimensions.update(bounds) + if coordinate_variables: + required_dimensions = set(coordinate_variables) logger.info( 'Variables being retrieved in prefetch request: ' @@ -152,9 +154,7 @@ def get_coordinate_variables( def update_dimension_variables( - prefetch_dataset: Dataset, - coordinates: Set[str], - varinfo: VarInfoFromDmr, + prefetch_dataset: Dataset, coordinates: Set[str], varinfo: VarInfoFromDmr, crs: CRS ) -> Dict[str, ndarray]: """Generate artificial 1D dimensions variable for each 2D dimension or coordinate variable @@ -167,12 +167,9 @@ def update_dimension_variables( (5) Generate the x-y dimscale array and return to the calling method """ - for coordinate_name in coordinates: - coordinate_variable = varinfo.get_variable(coordinate_name) - if prefetch_dataset[coordinate_variable.full_name_path][:].ndim > 1: - col_size = prefetch_dataset[coordinate_variable.full_name_path][:].shape[0] - row_size = prefetch_dataset[coordinate_variable.full_name_path][:].shape[1] - crs = get_variable_crs(coordinate_name, varinfo) + row_size, col_size = get_row_col_sizes_from_coordinate_datasets( + prefetch_dataset, coordinates, varinfo + ) geo_grid_corners = get_geo_grid_corners(prefetch_dataset, coordinates, varinfo) @@ -198,6 +195,27 @@ def update_dimension_variables( return {'projected_y': y_dim, 'projected_x': x_dim} +def get_row_col_sizes_from_coordinate_datasets( + prefetch_dataset: Dataset, + coordinates: Set[str], + varinfo: VarInfoFromDmr, +) -> Tuple[int, int]: + """ + This method returns the row and column sizes of the coordinate datasets + + """ + lat_arr, lon_arr = get_lat_lon_arrays(prefetch_dataset, coordinates, varinfo) + if lat_arr.ndim > 1: + col_size = lat_arr.shape[0] + row_size = lat_arr.shape[1] + if (lon_arr.shape[0] != lat_arr.shape[0]) or (lon_arr.shape[1] != lat_arr.shape[1]): + raise IrregularCoordinateDatasets(lon_arr.shape, lat_arr.shape) + if lat_arr.ndim and lon_arr.ndim == 1: + col_size = lat_arr.size + row_size = lon_arr.size + return row_size, col_size + + def is_latitude_ascending( prefetch_dataset: Dataset, coordinates: Set[str], diff --git a/hoss/exceptions.py b/hoss/exceptions.py index f92abc4..5745f7b 100644 --- a/hoss/exceptions.py +++ b/hoss/exceptions.py @@ -138,6 +138,21 @@ def __init__(self, referring_variable): ) +class IrregularCoordinateDatasets(CustomError): + """This exception is raised when HOSS tries to get latitude and longitude + datasets and they are missing or empty. These datasets are referred to + in the science variables with coordinate attributes. + + """ + + def __init__(self, longitude_shape, latitude_shape): + super().__init__( + 'IrregularCoordinateDatasets', + f'Longitude dataset shape: "{longitude_shape}"' + f'does not match the latitude dataset shape: "{latitude_shape}"', + ) + + class UnsupportedShapeFileFormat(CustomError): """This exception is raised when the shape file included in the input Harmony message is not GeoJSON. diff --git a/hoss/spatial.py b/hoss/spatial.py index 899fd22..107d0ef 100644 --- a/hoss/spatial.py +++ b/hoss/spatial.py @@ -80,25 +80,14 @@ def get_spatial_index_ranges( around the exterior of the user-defined GeoJSON shape, to ensure the correct extents are derived. - For projected grids which do not follow CF standards, the projected - dimension scales are computed based on the values in the coordinate - datasets if they are available. The geocorners are obtained from the - coordinate datasets and converted to projected meters based on the crs - of the product. The dimension scales are then computed based on the - grid size and grid resolution - """ bounding_box = get_harmony_message_bbox(harmony_message) index_ranges = {} - coordinate_variables = [] geographic_dimensions = varinfo.get_geographic_spatial_dimensions( required_variables ) projected_dimensions = varinfo.get_projected_spatial_dimensions(required_variables) - if (not geographic_dimensions) and (not projected_dimensions): - coordinate_variables = get_coordinate_variables(varinfo, required_variables) - non_spatial_variables = required_variables.difference( varinfo.get_spatial_dimensions(required_variables) ) @@ -115,7 +104,8 @@ def get_spatial_index_ranges( index_ranges[dimension] = get_geographic_index_range( dimension, varinfo, dimensions_file, bounding_box ) - if projected_dimensions or coordinate_variables: + + if projected_dimensions: for non_spatial_variable in non_spatial_variables: index_ranges.update( get_projected_x_y_index_ranges( @@ -125,20 +115,35 @@ def get_spatial_index_ranges( index_ranges, bounding_box=bounding_box, shape_file_path=shape_file_path, - coordinates=coordinate_variables, ) ) - return index_ranges + + if (not geographic_dimensions) and (not projected_dimensions): + coordinate_variables = get_coordinate_variables(varinfo, required_variables) + if coordinate_variables: + for non_spatial_variable in non_spatial_variables: + index_ranges.update( + get_x_y_index_ranges_from_coordinates( + non_spatial_variable, + varinfo, + dimensions_file, + coordinate_variables, + index_ranges, + bounding_box=bounding_box, + shape_file_path=shape_file_path, + ) + ) + + return index_ranges def get_projected_x_y_index_ranges( non_spatial_variable: str, varinfo: VarInfoFromDmr, - dimension_datasets: Dataset, + dimensions_file: Dataset, index_ranges: IndexRanges, bounding_box: BBox = None, shape_file_path: str = None, - coordinates: Set[str] = set(), ) -> IndexRanges: """This function returns a dictionary containing the minimum and maximum index ranges for a pair of projection x and y coordinates, e.g.: @@ -157,47 +162,116 @@ def get_projected_x_y_index_ranges( projected coordinate points. """ - if not coordinates: - projected_x, projected_y = get_projected_x_y_variables( - varinfo, non_spatial_variable + projected_x, projected_y = get_projected_x_y_variables( + varinfo, non_spatial_variable + ) + + if ( + projected_x is not None + and projected_y is not None + and not set((projected_x, projected_y)).issubset(set(index_ranges.keys())) + ): + crs = get_variable_crs(non_spatial_variable, varinfo) + + x_y_extents = get_projected_x_y_extents( + dimensions_file[projected_x][:], + dimensions_file[projected_y][:], + crs, + shape_file=shape_file_path, + bounding_box=bounding_box, ) - else: - projected_x = 'projected_x' - projected_y = 'projected_y' - override_dimension_datasets = update_dimension_variables( - dimension_datasets, - coordinates, - varinfo, + x_bounds = get_dimension_bounds(projected_x, varinfo, dimensions_file) + y_bounds = get_dimension_bounds(projected_y, varinfo, dimensions_file) + + x_index_ranges = get_dimension_index_range( + dimensions_file[projected_x][:], + x_y_extents['x_min'], + x_y_extents['x_max'], + bounds_values=x_bounds, ) - dimension_datasets = override_dimension_datasets + + y_index_ranges = get_dimension_index_range( + dimensions_file[projected_y][:], + x_y_extents['y_min'], + x_y_extents['y_max'], + bounds_values=y_bounds, + ) + + x_y_index_ranges = {projected_x: x_index_ranges, projected_y: y_index_ranges} + else: + x_y_index_ranges = {} + + return x_y_index_ranges + + +def get_x_y_index_ranges_from_coordinates( + non_spatial_variable: str, + varinfo: VarInfoFromDmr, + dimension_datasets: Dataset, + coordinates: Set[str], + index_ranges: IndexRanges, + bounding_box: BBox = None, + shape_file_path: str = None, +) -> IndexRanges: + """This function returns a dictionary containing the minimum and maximum + index ranges for a pair of projection x and y coordinates, e.g.: + + index_ranges = {'/x': (20, 42), '/y': (31, 53)} + + This method is called when the CF standards are not followed in the source + granule and only coordinate datasets are provided. + The coordinate datasets along with the crs is used to calculate the x-y + projected dimension scales. + The dimensions of the input, non-spatial variable are checked + for associated projection x and y coordinates. If these are present, + and they have not already been added to the `index_ranges` cache, the + extents of the input spatial subset are determined in these projected + coordinates. This requires the derivation of a minimum resolution of + the target grid in geographic coordinates. Points must be placed along + the exterior of the spatial subset shape. All points are then projected + from a geographic Coordinate Reference System (CRS) to the target grid + CRS. The minimum and maximum values are then derived from these + projected coordinate points. + + """ + crs = get_variable_crs(non_spatial_variable, varinfo) + + projected_x = 'projected_x' + projected_y = 'projected_y' + override_dimension_datasets = update_dimension_variables( + dimension_datasets, coordinates, varinfo, crs + ) if ( projected_x is not None and projected_y is not None and not set((projected_x, projected_y)).issubset(set(index_ranges.keys())) ): - crs = get_variable_crs(non_spatial_variable, varinfo) x_y_extents = get_projected_x_y_extents( - dimension_datasets[projected_x][:], - dimension_datasets[projected_y][:], + override_dimension_datasets[projected_x][:], + override_dimension_datasets[projected_y][:], crs, shape_file=shape_file_path, bounding_box=bounding_box, ) - x_bounds = get_dimension_bounds(projected_x, varinfo, dimension_datasets) - y_bounds = get_dimension_bounds(projected_y, varinfo, dimension_datasets) + x_bounds = get_dimension_bounds( + projected_x, varinfo, override_dimension_datasets + ) + y_bounds = get_dimension_bounds( + projected_y, varinfo, override_dimension_datasets + ) x_index_ranges = get_dimension_index_range( - dimension_datasets[projected_x][:], + override_dimension_datasets[projected_x][:], x_y_extents['x_min'], x_y_extents['x_max'], bounds_values=x_bounds, ) y_index_ranges = get_dimension_index_range( - dimension_datasets[projected_y][:], + override_dimension_datasets[projected_y][:], x_y_extents['y_min'], x_y_extents['y_max'], bounds_values=y_bounds, diff --git a/hoss/subset.py b/hoss/subset.py index 917b390..a08e590 100644 --- a/hoss/subset.py +++ b/hoss/subset.py @@ -29,7 +29,6 @@ from hoss.temporal import get_temporal_index_ranges from hoss.utilities import ( download_url, - format_dictionary_string, format_variable_set_string, get_opendap_nc4, ) From f07b544fd6dca4e8e9af53690a645c54124c9db7 Mon Sep 17 00:00:00 2001 From: sudhamurthy Date: Tue, 1 Oct 2024 03:04:53 -0400 Subject: [PATCH 17/41] DAS-2232 - fixed assumption of top left origin --- hoss/dimension_utilities.py | 43 +++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/hoss/dimension_utilities.py b/hoss/dimension_utilities.py index f4ed02c..5d68578 100644 --- a/hoss/dimension_utilities.py +++ b/hoss/dimension_utilities.py @@ -184,10 +184,14 @@ def update_dimension_variables( y_resolution = (y_max - y_min) / col_size # create the xy dim scales - x_dim = np.arange(x_min, x_max, x_resolution) + lat_asc, lon_asc = is_lat_lon_ascending(prefetch_dataset, coordinates, varinfo) - # The origin is usually the top left and Y values are in decreasing order. - if is_latitude_ascending(prefetch_dataset, coordinates, varinfo): + if lon_asc: + x_dim = np.arange(x_min, x_max, x_resolution) + else: + x_dim = np.arange(x_min, x_max, -x_resolution) + + if lat_asc: y_dim = np.arange(y_max, y_min, y_resolution) else: y_dim = np.arange(y_max, y_min, -y_resolution) @@ -216,18 +220,19 @@ def get_row_col_sizes_from_coordinate_datasets( return row_size, col_size -def is_latitude_ascending( +def is_lat_lon_ascending( prefetch_dataset: Dataset, coordinates: Set[str], varinfo: VarInfoFromDmr, -) -> bool: +) -> tuple[bool, bool]: """ - Checks if the latitude cooordinate datasets have values + Checks if the latitude and longitude cooordinate datasets have values that are ascending """ lat_arr, lon_arr = get_lat_lon_arrays(prefetch_dataset, coordinates, varinfo) lat_col = lat_arr[:, 0] - return is_dimension_ascending(lat_col) + lon_row = lon_arr[0, :] + return is_dimension_ascending(lat_col), is_dimension_ascending(lon_row) def get_lat_lon_arrays( @@ -288,11 +293,11 @@ def get_geo_grid_corners( # get the index of the minimum longitude after checking for invalid entries top_left_col_idx = lon_row_valid_indices[lon_row[lon_row_valid_indices].argmin()] - minlon = lon_row[top_left_col_idx] + min_lon = lon_row[top_left_col_idx] # get the index of the maximum longitude after checking for invalid entries top_right_col_idx = lon_row_valid_indices[lon_row[lon_row_valid_indices].argmax()] - maxlon = lon_row[top_right_col_idx] + max_lon = lon_row[top_right_col_idx] # get the last valid longitude column to get the latitude array lat_col = lat_arr[:, top_right_col_idx] @@ -307,22 +312,22 @@ def get_geo_grid_corners( bottom_right_row_idx = lat_col_valid_indices[ lat_col[lat_col_valid_indices].argmin() ] - minlat = lat_col[bottom_right_row_idx] + min_lat = lat_col[bottom_right_row_idx] # get the index of maximum latitude after checking for valid values top_right_row_idx = lat_col_valid_indices[lat_col[lat_col_valid_indices].argmax()] - maxlat = lat_col[top_right_row_idx] + max_lat = lat_col[top_right_row_idx] - topleft_corner = [minlon, maxlat] - topright_corner = [maxlon, maxlat] - bottomright_corner = [maxlon, minlat] - bottomleft_corner = [minlon, minlat] + top_left_corner = [min_lon, max_lat] + top_right_corner = [max_lon, max_lat] + bottom_right_corner = [max_lon, min_lat] + bottom_left_corner = [min_lon, min_lat] geo_grid_corners = [ - topleft_corner, - topright_corner, - bottomright_corner, - bottomleft_corner, + top_left_corner, + top_right_corner, + bottom_right_corner, + bottom_left_corner, ] return geo_grid_corners From 3b453e552cc814c0353a9efeacd3b06e292a6448 Mon Sep 17 00:00:00 2001 From: sudhamurthy Date: Tue, 1 Oct 2024 04:23:56 -0400 Subject: [PATCH 18/41] DAS=2232 - some refactoring --- hoss/dimension_utilities.py | 53 +++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/hoss/dimension_utilities.py b/hoss/dimension_utilities.py index 5d68578..5fc9653 100644 --- a/hoss/dimension_utilities.py +++ b/hoss/dimension_utilities.py @@ -106,9 +106,9 @@ def get_prefetch_variables( return required_dimensions_nc4 -def get_override_projected_dimensions( +def get_override_projected_dimension_name( varinfo: VarInfoFromDmr, - override_variable_name: str, + variable_name: str, ) -> str | None: """returns the x-y projection variable names that would match the geo coordinate names. The `latitude` coordinate @@ -117,7 +117,7 @@ def get_override_projected_dimensions( 'projected_x' """ - override_variable = varinfo.get_variable(override_variable_name) + override_variable = varinfo.get_variable(variable_name) if override_variable is not None: if override_variable.is_latitude(): projected_dimension_name = 'projected_y' @@ -128,6 +128,29 @@ def get_override_projected_dimensions( return projected_dimension_name +def get_override_projected_dimensions( + varinfo: VarInfoFromDmr, + variable_name: str, +) -> list[str]: + """ + Returns the projected dimensions names from coordinate variables + """ + coordinate_variables = get_coordinate_variables(varinfo, [variable_name]) + if coordinate_variables: + override_dimensions = [] + for coordinate in coordinate_variables: + override_dimensions.append( + get_override_projected_dimension_name(varinfo, coordinate) + ) + else: + # if the override is the variable + override = get_override_projected_dimension_name(varinfo, variable_name) + override_dimensions = ['projected_y', 'projected_x'] + if override is not None and override not in override_dimensions: + override_dimensions = [] + return override_dimensions + + def get_coordinate_variables( varinfo: VarInfoFromDmr, requested_variables: Set[str], @@ -137,7 +160,6 @@ def get_coordinate_variables( [latitude, longitude] """ - # try: coordinate_variables_set = varinfo.get_references_for_attribute( requested_variables, 'coordinates' ) @@ -149,8 +171,6 @@ def get_coordinate_variables( coordinate_variables.insert(1, coordinate) return coordinate_variables - # except AttributeError: - # return set() def update_dimension_variables( @@ -647,7 +667,8 @@ def add_index_range( the antimeridian or Prime Meridian) will have a minimum index greater than the maximum index. In this case the full dimension range should be requested, as the related values will be masked before returning the - output to the user. + output to the user. When the dimensions are not available, the coordinate + variables are used to calculate the projected dimensions. """ variable = varinfo.get_variable(variable_name) @@ -655,22 +676,8 @@ def add_index_range( if variable.dimensions: range_strings = get_range_strings(variable.dimensions, index_ranges) else: - coordinate_variables = get_coordinate_variables(varinfo, [variable_name]) - if coordinate_variables: - dimensions = [] - for coordinate in coordinate_variables: - dimensions.append( - get_override_projected_dimensions(varinfo, coordinate) - ) - range_strings = get_range_strings(dimensions, index_ranges) - else: - # if the override is the variable - override = get_override_projected_dimensions(varinfo, variable_name) - dimensions = ['projected_y', 'projected_x'] - if override is not None and override in dimensions: - range_strings = get_range_strings(dimensions, index_ranges) - else: - range_strings.append('[]') + override_dimensions = get_override_projected_dimensions(varinfo, variable_name) + range_strings = get_range_strings(override_dimensions, index_ranges) if all(range_string == '[]' for range_string in range_strings): indices_string = '' From 681b20d15c611e7cddb0ec904ded0038c63db45c Mon Sep 17 00:00:00 2001 From: sudhamurthy Date: Tue, 1 Oct 2024 10:27:33 -0400 Subject: [PATCH 19/41] DAS-2232 - updates to hoss_config.json description fields --- hoss/dimension_utilities.py | 3 --- hoss/hoss_config.json | 17 +++++++++-------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/hoss/dimension_utilities.py b/hoss/dimension_utilities.py index 5fc9653..4f98ebb 100644 --- a/hoss/dimension_utilities.py +++ b/hoss/dimension_utilities.py @@ -295,9 +295,6 @@ def get_geo_grid_corners( if not lon_arr.size: raise MissingCoordinateDataset('longitude') - # skip fill values when calculating min values - # topleft = minlon, maxlat - # bottomright = maxlon, minlat top_left_row_idx = 0 top_left_col_idx = 0 diff --git a/hoss/hoss_config.json b/hoss/hoss_config.json index 8f908ee..1f16dbe 100644 --- a/hoss/hoss_config.json +++ b/hoss/hoss_config.json @@ -156,7 +156,7 @@ "Value": "/EASE2_global_projection" } ], - "_Description": "Some versions of these collections omit global grid mapping information" + "_Description": "SMAP L3 collections omit global grid mapping information" }, { "Applicability": { @@ -168,7 +168,7 @@ "Value": "/EASE2_polar_projection" } ], - "_Description": "Some versions of these collections omit polar grid mapping information" + "_Description": "SMAP L3 collections omit polar grid mapping information" } ] }, @@ -188,7 +188,7 @@ "Value": "/EASE2_global_projection" } ], - "_Description": "Some versions of these collections omit global grid mapping information" + "_Description": "SMAP L3 collections omit global grid mapping information" }, { "Applicability": { @@ -200,7 +200,8 @@ "Value": "/EASE2_polar_projection" } ], - "_Description": "Some versions of these collections omit polar grid mapping information" + "_Description": "SMAP L3 collections omit polar grid mapping information" + } ] }, @@ -215,7 +216,7 @@ "Value": "/EASE2_polar_projection" } ], - "_Description": "Some versions of these collections omit polar grid mapping information" + "_Description": "SMAP L3 collections omit polar grid mapping information" }, { "Applicability": { @@ -228,7 +229,7 @@ "Value": "/EASE2_global_projection" } ], - "_Description": "Some versions of these collections omit global grid mapping information" + "_Description": "SMAP L3 collections omit global grid mapping information" }, { "Applicability": { @@ -262,7 +263,7 @@ "Value": 0.0 } ], - "_Description": "Some versions of these collections omit global grid mapping information" + "_Description": "Provide missing global grid mapping attributes for SMAP L3 collections." }, { "Applicability": { @@ -289,7 +290,7 @@ "Value": 0.0 } ], - "_Description": "Some versions of these collections omit polar grid mapping information" + "_Description": "Provide missing polar grid mapping attributes for SMAP L3 collections." } ] }, From 5d609c918edfb49e74b5a1f8b5c78073096e2488 Mon Sep 17 00:00:00 2001 From: sudhamurthy Date: Tue, 1 Oct 2024 11:06:32 -0400 Subject: [PATCH 20/41] DAS-2232 - updated the names for required_dimensions to required_variables --- hoss/dimension_utilities.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/hoss/dimension_utilities.py b/hoss/dimension_utilities.py index 4f98ebb..98fdf79 100644 --- a/hoss/dimension_utilities.py +++ b/hoss/dimension_utilities.py @@ -74,7 +74,7 @@ def get_prefetch_variables( access_token: str, config: Config, ) -> tuple[str, set[str]] | str: - """Determine the dimensions that need to be "pre-fetched" from OPeNDAP in + """Determine the variables that need to be "pre-fetched" from OPeNDAP in order to derive index ranges upon them. Initially, this was just spatial and temporal dimensions, but to support generic dimension subsets, all required dimensions must be prefetched, along with any @@ -83,27 +83,27 @@ def get_prefetch_variables( variables will be prefetched and used to calculate dimension-scale values """ - required_dimensions = varinfo.get_required_dimensions(required_variables) - if required_dimensions: - bounds = varinfo.get_references_for_attribute(required_dimensions, 'bounds') - required_dimensions.update(bounds) + required_variables = varinfo.get_required_dimensions(required_variables) + if required_variables: + bounds = varinfo.get_references_for_attribute(required_variables, 'bounds') + required_variables.update(bounds) else: coordinate_variables = get_coordinate_variables(varinfo, required_variables) if coordinate_variables: - required_dimensions = set(coordinate_variables) + required_variables = set(coordinate_variables) logger.info( 'Variables being retrieved in prefetch request: ' - f'{format_variable_set_string(required_dimensions)}' + f'{format_variable_set_string(required_variables)}' ) - required_dimensions_nc4 = get_opendap_nc4( - opendap_url, required_dimensions, output_dir, logger, access_token, config + required_variables_nc4 = get_opendap_nc4( + opendap_url, required_variables, output_dir, logger, access_token, config ) # Create bounds variables if necessary. - add_bounds_variables(required_dimensions_nc4, required_dimensions, varinfo, logger) - return required_dimensions_nc4 + add_bounds_variables(required_variables_nc4, required_variables, varinfo, logger) + return required_variables_nc4 def get_override_projected_dimension_name( From 91c51c063f603a373d70284f2b402254a45f0c4d Mon Sep 17 00:00:00 2001 From: sudhamurthy Date: Tue, 1 Oct 2024 12:50:15 -0400 Subject: [PATCH 21/41] DAS-2232 - added exception if one of the coordinate datasets are missing --- hoss/dimension_utilities.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/hoss/dimension_utilities.py b/hoss/dimension_utilities.py index 98fdf79..22c357f 100644 --- a/hoss/dimension_utilities.py +++ b/hoss/dimension_utilities.py @@ -146,7 +146,7 @@ def get_override_projected_dimensions( # if the override is the variable override = get_override_projected_dimension_name(varinfo, variable_name) override_dimensions = ['projected_y', 'projected_x'] - if override is not None and override not in override_dimensions: + if (override is None) or (override not in override_dimensions): override_dimensions = [] return override_dimensions @@ -164,11 +164,21 @@ def get_coordinate_variables( requested_variables, 'coordinates' ) coordinate_variables = [] + contains_latitude = False + contains_longitude = False for coordinate in coordinate_variables_set: if varinfo.get_variable(coordinate).is_latitude(): coordinate_variables.insert(0, coordinate) + contains_latitude = True elif varinfo.get_variable(coordinate).is_longitude(): coordinate_variables.insert(1, coordinate) + contains_longitude = True + + if coordinate_variables: + if not contains_latitude: + raise MissingCoordinateDataset('latitude') + if not contains_longitude: + raise MissingCoordinateDataset('longitude') return coordinate_variables @@ -674,7 +684,8 @@ def add_index_range( range_strings = get_range_strings(variable.dimensions, index_ranges) else: override_dimensions = get_override_projected_dimensions(varinfo, variable_name) - range_strings = get_range_strings(override_dimensions, index_ranges) + if override_dimensions: + range_strings = get_range_strings(override_dimensions, index_ranges) if all(range_string == '[]' for range_string in range_strings): indices_string = '' From 2296a35f4a0a94038839e205a434051c2590ca21 Mon Sep 17 00:00:00 2001 From: sudhamurthy Date: Tue, 1 Oct 2024 13:29:29 -0400 Subject: [PATCH 22/41] DAS-2232 - updated CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f977834..eb8b4b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ### 2024-09-10 This version of HOSS provides support for products that do not comply with CF standards like SMAP L3. +The SMAP L3 products do not have grid mapping variable to provide the projection information. +They also do not have x-y dimension scales for the projected grid. New methods are added to retrieve dimension scales from coordinate attributes and grid mappings, using overrides specified in the hoss_config.json configuration file. - `get_coordinate_variables` gets coordinate datasets when the dimension scales are not present in From 2efc4c752d48f24687465441d4b82709d0a4c0cb Mon Sep 17 00:00:00 2001 From: sudhamurthy Date: Tue, 1 Oct 2024 13:36:38 -0400 Subject: [PATCH 23/41] DAS-2232 - small bug fix in get_override_projection_dimension_name --- hoss/dimension_utilities.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/hoss/dimension_utilities.py b/hoss/dimension_utilities.py index 22c357f..f537850 100644 --- a/hoss/dimension_utilities.py +++ b/hoss/dimension_utilities.py @@ -118,13 +118,12 @@ def get_override_projected_dimension_name( """ override_variable = varinfo.get_variable(variable_name) + projected_dimension_name = None if override_variable is not None: if override_variable.is_latitude(): projected_dimension_name = 'projected_y' elif override_variable.is_longitude(): projected_dimension_name = 'projected_x' - else: - projected_dimension_name = None return projected_dimension_name From f628166ed1d8bc3d1e879ba73332c1475cacf728 Mon Sep 17 00:00:00 2001 From: sudhamurthy Date: Tue, 1 Oct 2024 18:15:26 -0400 Subject: [PATCH 24/41] DAS-2232 - updates to comments --- hoss/exceptions.py | 4 ++-- hoss/spatial.py | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/hoss/exceptions.py b/hoss/exceptions.py index 5745f7b..956ac38 100644 --- a/hoss/exceptions.py +++ b/hoss/exceptions.py @@ -125,8 +125,8 @@ def __init__(self, referring_variable): class MissingValidCoordinateDataset(CustomError): """This exception is raised when HOSS tries to get latitude and longitude - datasets and they are missing or empty. These datasets are referred to - in the science variables with coordinate attributes. + datasets and they have fill values to the extent that it cannot be used. + These datasets are referred in the science variables with coordinate attributes. """ diff --git a/hoss/spatial.py b/hoss/spatial.py index 107d0ef..3bc4f00 100644 --- a/hoss/spatial.py +++ b/hoss/spatial.py @@ -80,6 +80,10 @@ def get_spatial_index_ranges( around the exterior of the user-defined GeoJSON shape, to ensure the correct extents are derived. + if geographic and projected dimensions are not specified in the granule, + the coordinate datasets are used to calculate the x-y dimensions and the index ranges + are calculated similar to a projected grid. + """ bounding_box = get_harmony_message_bbox(harmony_message) index_ranges = {} From ebac2a043317b46fab97ceef8c1dc684a441b8de Mon Sep 17 00:00:00 2001 From: sudhamurthy Date: Wed, 2 Oct 2024 11:58:44 -0400 Subject: [PATCH 25/41] DAS-2232 - added check for fillvalue --- hoss/dimension_utilities.py | 76 ++++++++++++++++++++++++++----------- hoss/hoss_config.json | 18 ++++++++- 2 files changed, 70 insertions(+), 24 deletions(-) diff --git a/hoss/dimension_utilities.py b/hoss/dimension_utilities.py index f537850..467e8a1 100644 --- a/hoss/dimension_utilities.py +++ b/hoss/dimension_utilities.py @@ -299,6 +299,7 @@ def get_geo_grid_corners( The fill values in the corner points still needs to be addressed. """ lat_arr, lon_arr = get_lat_lon_arrays(prefetch_dataset, coordinates, varinfo) + if not lat_arr.size: raise MissingCoordinateDataset('latitude') if not lon_arr.size: @@ -307,15 +308,11 @@ def get_geo_grid_corners( top_left_row_idx = 0 top_left_col_idx = 0 + lat_fill, lon_fill = get_fill_values_for_coordinates(coordinates, varinfo) + # get the first row from the longitude dataset lon_row = lon_arr[top_left_row_idx, :] - lon_row_valid_indices = np.where((lon_row >= -180.0) & (lon_row <= 180.0))[0] - - # if the first row does not have valid indices, - # should go down to the next row. We throw an exception - # for now till that gets addressed - if not lon_row_valid_indices.size: - raise MissingValidCoordinateDataset('longitude') + lon_row_valid_indices = get_valid_indices(lon_row, lon_fill, 'longitude') # get the index of the minimum longitude after checking for invalid entries top_left_col_idx = lon_row_valid_indices[lon_row[lon_row_valid_indices].argmin()] @@ -327,12 +324,7 @@ def get_geo_grid_corners( # get the last valid longitude column to get the latitude array lat_col = lat_arr[:, top_right_col_idx] - lat_col_valid_indices = np.where((lat_col >= -90.0) & (lat_col <= 90.0))[0] - - # if the longitude values are invalid, should check the other columns - # We throw an exception for now till that gets addressed - if not lat_col_valid_indices.size: - raise MissingValidCoordinateDataset('latitude') + lat_col_valid_indices = get_valid_indices(lat_col, lat_fill, 'latitude') # get the index of minimum latitude after checking for valid values bottom_right_row_idx = lat_col_valid_indices[ @@ -344,20 +336,60 @@ def get_geo_grid_corners( top_right_row_idx = lat_col_valid_indices[lat_col[lat_col_valid_indices].argmax()] max_lat = lat_col[top_right_row_idx] - top_left_corner = [min_lon, max_lat] - top_right_corner = [max_lon, max_lat] - bottom_right_corner = [max_lon, min_lat] - bottom_left_corner = [min_lon, min_lat] - geo_grid_corners = [ - top_left_corner, - top_right_corner, - bottom_right_corner, - bottom_left_corner, + [min_lon, max_lat], + [max_lon, max_lat], + [max_lon, min_lat], + [min_lon, min_lat], ] return geo_grid_corners +def get_valid_indices( + coordinate_row_col: ndarray, coordinate_fill: float, coordinate_name: str +) -> ndarray: + """ + Returns indices of a valid array without fill values + """ + if coordinate_fill: + valid_indices = np.where(coordinate_row_col != coordinate_fill)[0] + elif coordinate_name == 'longitude': + valid_indices = np.where( + (coordinate_row_col >= -180.0) & (coordinate_row_col <= 180.0) + )[0] + elif coordinate_name == 'latitude': + valid_indices = np.where( + (coordinate_row_col >= -90.0) & (coordinate_row_col <= 90.0) + )[0] + + # if the first row does not have valid indices, + # should go down to the next row. We throw an exception + # for now till that gets addressed + if not valid_indices.size: + raise MissingValidCoordinateDataset(coordinate_name) + return valid_indices + + +def get_fill_values_for_coordinates( + coordinates: Set[str], varinfo: VarInfoFromDmr +) -> float | None: + """ + returns fill values for the variable. If it does not exist + checks for the overrides from the json file. If there is no + overrides, returns None + """ + for coordinate in coordinates: + coordinate_variable = varinfo.get_variable(coordinate) + if coordinate_variable.is_latitude(): + lat_fill_value = coordinate_variable.get_attribute_value('_fillValue') + elif coordinate_variable.is_longitude(): + lon_fill_value = coordinate_variable.get_attribute_value('_fillValue') + # if fill_value is None: + # check if there are overrides in hoss_config.json using varinfo + # else + return lat_fill_value, lon_fill_value + + def add_bounds_variables( dimensions_nc4: str, required_dimensions: Set[str], diff --git a/hoss/hoss_config.json b/hoss/hoss_config.json index 1f16dbe..70e148f 100644 --- a/hoss/hoss_config.json +++ b/hoss/hoss_config.json @@ -302,8 +302,22 @@ }, "Attributes": [ { - "Name": "_fill", - "Value": "-9999" + "Name": "_FillValue", + "Value": "-9999.0" + } + ], + "_Description": "Ensure metadata fill value matches what is present in arrays." + }, + { + "Applicability": { + "Mission": "SMAP", + "ShortNamePath": "SPL3SM(A|P|AP|P_E)", + "Variable_Pattern": "/Soil_Moisture_Retrieval_(Data|Data_AM|Data_Polar_AM)/(latitude|longitude).*" + }, + "Attributes": [ + { + "Name": "_FillValue", + "Value": "-9999.0" } ], "_Description": "Ensure metadata fill value matches what is present in arrays." From 3b6d6054afcbd526134b7ccc66f8accc280836fb Mon Sep 17 00:00:00 2001 From: sudhamurthy Date: Wed, 2 Oct 2024 14:40:08 -0400 Subject: [PATCH 26/41] DAS-2232 - comments and minor changes --- hoss/exceptions.py | 2 +- hoss/spatial.py | 39 +++++++++++++-------------------------- 2 files changed, 14 insertions(+), 27 deletions(-) diff --git a/hoss/exceptions.py b/hoss/exceptions.py index 956ac38..16792b0 100644 --- a/hoss/exceptions.py +++ b/hoss/exceptions.py @@ -57,7 +57,7 @@ class InvalidRequestedRange(CustomError): def __init__(self): super().__init__( 'InvalidRequestedRange', - 'Input request specified range outside supported ' 'dimension range', + 'Input request specified range outside supported dimension range', ) diff --git a/hoss/spatial.py b/hoss/spatial.py index 3bc4f00..704a9bd 100644 --- a/hoss/spatial.py +++ b/hoss/spatial.py @@ -212,31 +212,27 @@ def get_projected_x_y_index_ranges( def get_x_y_index_ranges_from_coordinates( non_spatial_variable: str, varinfo: VarInfoFromDmr, - dimension_datasets: Dataset, + prefetch_coordinate_datasets: Dataset, coordinates: Set[str], index_ranges: IndexRanges, bounding_box: BBox = None, shape_file_path: str = None, ) -> IndexRanges: """This function returns a dictionary containing the minimum and maximum - index ranges for a pair of projection x and y coordinates, e.g.: + index ranges for a pair of lat/lon coordinates, e.g.: index_ranges = {'/x': (20, 42), '/y': (31, 53)} - This method is called when the CF standards are not followed in the source - granule and only coordinate datasets are provided. - The coordinate datasets along with the crs is used to calculate the x-y - projected dimension scales. - The dimensions of the input, non-spatial variable are checked - for associated projection x and y coordinates. If these are present, - and they have not already been added to the `index_ranges` cache, the - extents of the input spatial subset are determined in these projected - coordinates. This requires the derivation of a minimum resolution of - the target grid in geographic coordinates. Points must be placed along - the exterior of the spatial subset shape. All points are then projected - from a geographic Coordinate Reference System (CRS) to the target grid - CRS. The minimum and maximum values are then derived from these - projected coordinate points. + This method is called when the CF standards are not followed + in the source granule and only coordinate datasets are provided. + The coordinate datasets along with the crs is used to calculate + the x-y projected dimension scales. The dimensions of the input, + non-spatial variable are checked for associated coordinates. If + these are present, and they have not already been added to the + `index_ranges` cache, the extents of the input spatial subset + are determined in these projected coordinates. The minimum and + maximum values are then derived from these projected coordinate + points. """ crs = get_variable_crs(non_spatial_variable, varinfo) @@ -244,7 +240,7 @@ def get_x_y_index_ranges_from_coordinates( projected_x = 'projected_x' projected_y = 'projected_y' override_dimension_datasets = update_dimension_variables( - dimension_datasets, coordinates, varinfo, crs + prefetch_coordinate_datasets, coordinates, varinfo, crs ) if ( @@ -261,24 +257,15 @@ def get_x_y_index_ranges_from_coordinates( bounding_box=bounding_box, ) - x_bounds = get_dimension_bounds( - projected_x, varinfo, override_dimension_datasets - ) - y_bounds = get_dimension_bounds( - projected_y, varinfo, override_dimension_datasets - ) - x_index_ranges = get_dimension_index_range( override_dimension_datasets[projected_x][:], x_y_extents['x_min'], x_y_extents['x_max'], - bounds_values=x_bounds, ) y_index_ranges = get_dimension_index_range( override_dimension_datasets[projected_y][:], x_y_extents['y_min'], x_y_extents['y_max'], - bounds_values=y_bounds, ) x_y_index_ranges = {projected_x: x_index_ranges, projected_y: y_index_ranges} else: From 802fe0ec04b853a7a7abb4718e2a05b0e8e6ed3c Mon Sep 17 00:00:00 2001 From: sudhamurthy Date: Thu, 3 Oct 2024 11:18:29 -0400 Subject: [PATCH 27/41] DAS-2232 - simplified get_coordinate_variables based on PR feedback --- hoss/dimension_utilities.py | 173 +++++++++++++++++++++--------------- hoss/spatial.py | 32 +++---- 2 files changed, 117 insertions(+), 88 deletions(-) diff --git a/hoss/dimension_utilities.py b/hoss/dimension_utilities.py index 467e8a1..0563536 100644 --- a/hoss/dimension_utilities.py +++ b/hoss/dimension_utilities.py @@ -73,7 +73,7 @@ def get_prefetch_variables( logger: Logger, access_token: str, config: Config, -) -> tuple[str, set[str]] | str: +) -> str: """Determine the variables that need to be "pre-fetched" from OPeNDAP in order to derive index ranges upon them. Initially, this was just spatial and temporal dimensions, but to support generic dimension @@ -83,33 +83,36 @@ def get_prefetch_variables( variables will be prefetched and used to calculate dimension-scale values """ - required_variables = varinfo.get_required_dimensions(required_variables) - if required_variables: - bounds = varinfo.get_references_for_attribute(required_variables, 'bounds') - required_variables.update(bounds) + prefetch_variables = varinfo.get_required_dimensions(required_variables) + if prefetch_variables: + bounds = varinfo.get_references_for_attribute(prefetch_variables, 'bounds') + prefetch_variables.update(bounds) else: - coordinate_variables = get_coordinate_variables(varinfo, required_variables) - if coordinate_variables: - required_variables = set(coordinate_variables) + latitude_coordinates, longitude_coordinates = get_coordinate_variables( + varinfo, required_variables + ) + + if latitude_coordinates and longitude_coordinates: + prefetch_variables = set(latitude_coordinates + longitude_coordinates) logger.info( 'Variables being retrieved in prefetch request: ' - f'{format_variable_set_string(required_variables)}' + f'{format_variable_set_string(prefetch_variables)}' ) - required_variables_nc4 = get_opendap_nc4( - opendap_url, required_variables, output_dir, logger, access_token, config + prefetch_variables_nc4 = get_opendap_nc4( + opendap_url, prefetch_variables, output_dir, logger, access_token, config ) # Create bounds variables if necessary. - add_bounds_variables(required_variables_nc4, required_variables, varinfo, logger) - return required_variables_nc4 + add_bounds_variables(prefetch_variables_nc4, prefetch_variables, varinfo, logger) + return prefetch_variables_nc4 def get_override_projected_dimension_name( varinfo: VarInfoFromDmr, variable_name: str, -) -> str | None: +) -> str: """returns the x-y projection variable names that would match the geo coordinate names. The `latitude` coordinate variable name gets converted to 'projected_y' dimension scale @@ -118,7 +121,7 @@ def get_override_projected_dimension_name( """ override_variable = varinfo.get_variable(variable_name) - projected_dimension_name = None + projected_dimension_name = '' if override_variable is not None: if override_variable.is_latitude(): projected_dimension_name = 'projected_y' @@ -134,18 +137,26 @@ def get_override_projected_dimensions( """ Returns the projected dimensions names from coordinate variables """ - coordinate_variables = get_coordinate_variables(varinfo, [variable_name]) - if coordinate_variables: + latitude_coordinates, longitude_coordinates = get_coordinate_variables( + varinfo, [variable_name] + ) + if latitude_coordinates and longitude_coordinates: + # there should be only 1 lat and lon coordinate for one variable override_dimensions = [] - for coordinate in coordinate_variables: - override_dimensions.append( - get_override_projected_dimension_name(varinfo, coordinate) - ) + override_dimensions.append( + get_override_projected_dimension_name(varinfo, latitude_coordinates[0]) + ) + override_dimensions.append( + get_override_projected_dimension_name(varinfo, longitude_coordinates[0]) + ) + else: # if the override is the variable - override = get_override_projected_dimension_name(varinfo, variable_name) + override_projected_dimension_name = get_override_projected_dimension_name( + varinfo, variable_name + ) override_dimensions = ['projected_y', 'projected_x'] - if (override is None) or (override not in override_dimensions): + if override_projected_dimension_name not in override_dimensions: override_dimensions = [] return override_dimensions @@ -153,7 +164,7 @@ def get_override_projected_dimensions( def get_coordinate_variables( varinfo: VarInfoFromDmr, requested_variables: Set[str], -) -> list[str]: +) -> tuple[list, list]: """This method returns coordinate variables that are referenced in the variables requested. It returns it in a specific order [latitude, longitude] @@ -162,28 +173,27 @@ def get_coordinate_variables( coordinate_variables_set = varinfo.get_references_for_attribute( requested_variables, 'coordinates' ) - coordinate_variables = [] - contains_latitude = False - contains_longitude = False - for coordinate in coordinate_variables_set: - if varinfo.get_variable(coordinate).is_latitude(): - coordinate_variables.insert(0, coordinate) - contains_latitude = True - elif varinfo.get_variable(coordinate).is_longitude(): - coordinate_variables.insert(1, coordinate) - contains_longitude = True - - if coordinate_variables: - if not contains_latitude: - raise MissingCoordinateDataset('latitude') - if not contains_longitude: - raise MissingCoordinateDataset('longitude') - - return coordinate_variables + + latitude_coordinate_variables = [ + coordinate + for coordinate in coordinate_variables_set + if varinfo.get_variable(coordinate).is_latitude() + ] + + longitude_coordinate_variables = [ + coordinate + for coordinate in coordinate_variables_set + if varinfo.get_variable(coordinate).is_longitude() + ] + + return latitude_coordinate_variables, longitude_coordinate_variables def update_dimension_variables( - prefetch_dataset: Dataset, coordinates: Set[str], varinfo: VarInfoFromDmr, crs: CRS + prefetch_dataset: Dataset, + latitude_coordinate: VariableFromDmr, + longitude_coordinate: VariableFromDmr, + crs: CRS, ) -> Dict[str, ndarray]: """Generate artificial 1D dimensions variable for each 2D dimension or coordinate variable @@ -196,11 +206,19 @@ def update_dimension_variables( (5) Generate the x-y dimscale array and return to the calling method """ + # if there is more than one grid, determine the lat/lon coordinate + # associated with this variable row_size, col_size = get_row_col_sizes_from_coordinate_datasets( - prefetch_dataset, coordinates, varinfo + prefetch_dataset, + latitude_coordinate, + longitude_coordinate, ) - geo_grid_corners = get_geo_grid_corners(prefetch_dataset, coordinates, varinfo) + geo_grid_corners = get_geo_grid_corners( + prefetch_dataset, + latitude_coordinate, + longitude_coordinate, + ) x_y_extents = get_x_y_extents_from_geographic_points(geo_grid_corners, crs) @@ -213,7 +231,11 @@ def update_dimension_variables( y_resolution = (y_max - y_min) / col_size # create the xy dim scales - lat_asc, lon_asc = is_lat_lon_ascending(prefetch_dataset, coordinates, varinfo) + lat_asc, lon_asc = is_lat_lon_ascending( + prefetch_dataset, + latitude_coordinate, + longitude_coordinate, + ) if lon_asc: x_dim = np.arange(x_min, x_max, x_resolution) @@ -230,14 +252,16 @@ def update_dimension_variables( def get_row_col_sizes_from_coordinate_datasets( prefetch_dataset: Dataset, - coordinates: Set[str], - varinfo: VarInfoFromDmr, + latitude_coordinate: VariableFromDmr, + longitude_coordinate: VariableFromDmr, ) -> Tuple[int, int]: """ This method returns the row and column sizes of the coordinate datasets """ - lat_arr, lon_arr = get_lat_lon_arrays(prefetch_dataset, coordinates, varinfo) + lat_arr, lon_arr = get_lat_lon_arrays( + prefetch_dataset, latitude_coordinate, longitude_coordinate + ) if lat_arr.ndim > 1: col_size = lat_arr.shape[0] row_size = lat_arr.shape[1] @@ -251,14 +275,16 @@ def get_row_col_sizes_from_coordinate_datasets( def is_lat_lon_ascending( prefetch_dataset: Dataset, - coordinates: Set[str], - varinfo: VarInfoFromDmr, + latitude_coordinate: VariableFromDmr, + longitude_coordinate: VariableFromDmr, ) -> tuple[bool, bool]: """ Checks if the latitude and longitude cooordinate datasets have values that are ascending """ - lat_arr, lon_arr = get_lat_lon_arrays(prefetch_dataset, coordinates, varinfo) + lat_arr, lon_arr = get_lat_lon_arrays( + prefetch_dataset, latitude_coordinate, longitude_coordinate + ) lat_col = lat_arr[:, 0] lon_row = lon_arr[0, :] return is_dimension_ascending(lat_col), is_dimension_ascending(lon_row) @@ -266,8 +292,8 @@ def is_lat_lon_ascending( def get_lat_lon_arrays( prefetch_dataset: Dataset, - coordinates: Set[str], - varinfo: VarInfoFromDmr, + latitude_coordinate: VariableFromDmr, + longitude_coordinate: VariableFromDmr, ) -> Tuple[ndarray, ndarray]: """ This method is used to return the lat lon arrays from a 2D @@ -275,20 +301,17 @@ def get_lat_lon_arrays( """ lat_arr = [] lon_arr = [] - for coordinate in coordinates: - coordinate_variable = varinfo.get_variable(coordinate) - if coordinate_variable.is_latitude(): - lat_arr = prefetch_dataset[coordinate_variable.full_name_path][:] - elif coordinate_variable.is_longitude(): - lon_arr = prefetch_dataset[coordinate_variable.full_name_path][:] + + lat_arr = prefetch_dataset[latitude_coordinate.full_name_path][:] + lon_arr = prefetch_dataset[longitude_coordinate.full_name_path][:] return lat_arr, lon_arr def get_geo_grid_corners( prefetch_dataset: Dataset, - coordinates: Set[str], - varinfo: VarInfoFromDmr, + latitude_coordinate: VariableFromDmr, + longitude_coordinate: VariableFromDmr, ) -> list[Tuple[float]]: """ This method is used to return the lat lon corners from a 2D @@ -298,7 +321,11 @@ def get_geo_grid_corners( are fill values in the corner points to go down to the next row and col The fill values in the corner points still needs to be addressed. """ - lat_arr, lon_arr = get_lat_lon_arrays(prefetch_dataset, coordinates, varinfo) + lat_arr, lon_arr = get_lat_lon_arrays( + prefetch_dataset, + latitude_coordinate, + longitude_coordinate, + ) if not lat_arr.size: raise MissingCoordinateDataset('latitude') @@ -308,7 +335,9 @@ def get_geo_grid_corners( top_left_row_idx = 0 top_left_col_idx = 0 - lat_fill, lon_fill = get_fill_values_for_coordinates(coordinates, varinfo) + lat_fill, lon_fill = get_fill_values_for_coordinates( + latitude_coordinate, longitude_coordinate + ) # get the first row from the longitude dataset lon_row = lon_arr[top_left_row_idx, :] @@ -371,21 +400,19 @@ def get_valid_indices( def get_fill_values_for_coordinates( - coordinates: Set[str], varinfo: VarInfoFromDmr + latitude_coordinate: VariableFromDmr, + longitude_coordinate: VariableFromDmr, ) -> float | None: """ returns fill values for the variable. If it does not exist checks for the overrides from the json file. If there is no overrides, returns None """ - for coordinate in coordinates: - coordinate_variable = varinfo.get_variable(coordinate) - if coordinate_variable.is_latitude(): - lat_fill_value = coordinate_variable.get_attribute_value('_fillValue') - elif coordinate_variable.is_longitude(): - lon_fill_value = coordinate_variable.get_attribute_value('_fillValue') - # if fill_value is None: - # check if there are overrides in hoss_config.json using varinfo + + lat_fill_value = latitude_coordinate.get_attribute_value('_fillValue') + lon_fill_value = longitude_coordinate.get_attribute_value('_fillValue') + # if fill_value is None: + # check if there are overrides in hoss_config.json using varinfo # else return lat_fill_value, lon_fill_value diff --git a/hoss/spatial.py b/hoss/spatial.py index 704a9bd..99434f0 100644 --- a/hoss/spatial.py +++ b/hoss/spatial.py @@ -27,7 +27,7 @@ from harmony.message import Message from netCDF4 import Dataset from numpy.ma.core import MaskedArray -from varinfo import VarInfoFromDmr +from varinfo import VariableFromDmr, VarInfoFromDmr from hoss.bbox_utilities import ( BBox, @@ -123,15 +123,18 @@ def get_spatial_index_ranges( ) if (not geographic_dimensions) and (not projected_dimensions): - coordinate_variables = get_coordinate_variables(varinfo, required_variables) - if coordinate_variables: - for non_spatial_variable in non_spatial_variables: + for non_spatial_variable in non_spatial_variables: + latitude_coordinates, longitude_coordinates = get_coordinate_variables( + varinfo, [non_spatial_variable] + ) + if latitude_coordinates and longitude_coordinates: index_ranges.update( get_x_y_index_ranges_from_coordinates( non_spatial_variable, varinfo, dimensions_file, - coordinate_variables, + varinfo.get_variable(latitude_coordinates[0]), + varinfo.get_variable(longitude_coordinates[0]), index_ranges, bounding_box=bounding_box, shape_file_path=shape_file_path, @@ -213,15 +216,16 @@ def get_x_y_index_ranges_from_coordinates( non_spatial_variable: str, varinfo: VarInfoFromDmr, prefetch_coordinate_datasets: Dataset, - coordinates: Set[str], + latitude_coordinate: VariableFromDmr, + longitude_coordinate: VariableFromDmr, index_ranges: IndexRanges, bounding_box: BBox = None, shape_file_path: str = None, ) -> IndexRanges: """This function returns a dictionary containing the minimum and maximum - index ranges for a pair of lat/lon coordinates, e.g.: + index ranges for the projected_x and projected_y recalculated dimension scales - index_ranges = {'/x': (20, 42), '/y': (31, 53)} + index_ranges = {'projected_x': (20, 42), 'projected_y': (31, 53)} This method is called when the CF standards are not followed in the source granule and only coordinate datasets are provided. @@ -236,18 +240,16 @@ def get_x_y_index_ranges_from_coordinates( """ crs = get_variable_crs(non_spatial_variable, varinfo) - projected_x = 'projected_x' projected_y = 'projected_y' override_dimension_datasets = update_dimension_variables( - prefetch_coordinate_datasets, coordinates, varinfo, crs + prefetch_coordinate_datasets, + latitude_coordinate, + longitude_coordinate, + crs, ) - if ( - projected_x is not None - and projected_y is not None - and not set((projected_x, projected_y)).issubset(set(index_ranges.keys())) - ): + if not set((projected_x, projected_y)).issubset(set(index_ranges.keys())): x_y_extents = get_projected_x_y_extents( override_dimension_datasets[projected_x][:], From 60fb22a098e173c77b50fbb043045e56ad06f877 Mon Sep 17 00:00:00 2001 From: sudhamurthy Date: Thu, 3 Oct 2024 13:38:41 -0400 Subject: [PATCH 28/41] DAS-2232 - added method to check for variables with no dimensions --- hoss/dimension_utilities.py | 14 ++++++++++++++ hoss/spatial.py | 38 +++++++++++++++++++------------------ 2 files changed, 34 insertions(+), 18 deletions(-) diff --git a/hoss/dimension_utilities.py b/hoss/dimension_utilities.py index 0563536..ec1512a 100644 --- a/hoss/dimension_utilities.py +++ b/hoss/dimension_utilities.py @@ -161,6 +161,20 @@ def get_override_projected_dimensions( return override_dimensions +def get_variables_with_anonymous_dims( + varinfo: VarInfoFromDmr, required_variables: set[str] +) -> bool: + """ + returns the list of required variables without any + dimensions + """ + return set( + required_variable + for required_variable in required_variables + if len(varinfo.get_variable(required_variable).dimensions) == 0 + ) + + def get_coordinate_variables( varinfo: VarInfoFromDmr, requested_variables: Set[str], diff --git a/hoss/spatial.py b/hoss/spatial.py index 99434f0..3c2ee2b 100644 --- a/hoss/spatial.py +++ b/hoss/spatial.py @@ -42,6 +42,7 @@ get_dimension_bounds, get_dimension_extents, get_dimension_index_range, + get_variables_with_anonymous_dims, update_dimension_variables, ) from hoss.projection_utilities import ( @@ -121,25 +122,26 @@ def get_spatial_index_ranges( shape_file_path=shape_file_path, ) ) - - if (not geographic_dimensions) and (not projected_dimensions): - for non_spatial_variable in non_spatial_variables: - latitude_coordinates, longitude_coordinates = get_coordinate_variables( - varinfo, [non_spatial_variable] - ) - if latitude_coordinates and longitude_coordinates: - index_ranges.update( - get_x_y_index_ranges_from_coordinates( - non_spatial_variable, - varinfo, - dimensions_file, - varinfo.get_variable(latitude_coordinates[0]), - varinfo.get_variable(longitude_coordinates[0]), - index_ranges, - bounding_box=bounding_box, - shape_file_path=shape_file_path, - ) + variables_with_anonymous_dims = get_variables_with_anonymous_dims( + varinfo, required_variables + ) + for variable_with_anonymous_dims in variables_with_anonymous_dims: + latitude_coordinates, longitude_coordinates = get_coordinate_variables( + varinfo, [variable_with_anonymous_dims] + ) + if latitude_coordinates and longitude_coordinates: + index_ranges.update( + get_x_y_index_ranges_from_coordinates( + variable_with_anonymous_dims, + varinfo, + dimensions_file, + varinfo.get_variable(latitude_coordinates[0]), + varinfo.get_variable(longitude_coordinates[0]), + index_ranges, + bounding_box=bounding_box, + shape_file_path=shape_file_path, ) + ) return index_ranges From 631dc243ec49329fbb8ba896967306fb9aacd9eb Mon Sep 17 00:00:00 2001 From: sudhamurthy Date: Thu, 3 Oct 2024 19:12:49 -0400 Subject: [PATCH 29/41] DAS-2232 - added unittest for get_variable_crs --- tests/data/SC_SPL3SMP_009.dmr | 2800 +++++++++++++++++++++++ tests/unit/test_projection_utilities.py | 20 + 2 files changed, 2820 insertions(+) create mode 100644 tests/data/SC_SPL3SMP_009.dmr diff --git a/tests/data/SC_SPL3SMP_009.dmr b/tests/data/SC_SPL3SMP_009.dmr new file mode 100644 index 0000000..14d4977 --- /dev/null +++ b/tests/data/SC_SPL3SMP_009.dmr @@ -0,0 +1,2800 @@ + + + + + 3.21.0-428 + + + 3.21.0-428 + + + libdap-3.21.0-103 + + + build_dmrpp -c /tmp/bes_conf_PXTZ -f /usr/share/hyrax/DATA/SMAP_L3_SM_P_20150410_R19240_001.h5 -r /tmp/dmr__MLbLWr -u OPeNDAP_DMRpp_DATA_ACCESS_URL -M + + + + + 98a1a16758d855ddc3f64213ed0eb3a7 + + + e3fcd32b9b8c270a5851619117536eb1 + + + + + The SMAP observatory houses an L-band radiometer that operates at 1.414 GHz and an L-band radar that operates at 1.225 GHz. The instruments share a rotating reflector antenna with a 6 meter aperture that scans over a 1000 km swath. The bus is a 3 axis stabilized spacecraft that provides momentum compensation for the rotating antenna. + + + 14.60000038 + + + SMAP + + + + + JPL CL#14-2285, JPL 400-1567 + + + SMAP Handbook + + + 2014-07-01 + + + + + JPL CL#14-2285, JPL 400-1567 + + + SMAP Handbook + + + 2014-07-01 + + + + + The SMAP 1.414 GHz L-Band Radiometer + + + L-Band Radiometer + + + SMAP RAD + + + + + The SMAP 1.225 GHz L-Band Radar Instrument + + + L-Band Synthetic Aperture Radar + + + SMAP SAR + + + + + JPL CL#14-2285, JPL 400-1567 + + + SMAP Handbook + + + 2014-07-01 + + + + + + soil_moisture + + + + Percentage of EASE2 grid cells with Retrieved Soil Moistures outside the Acceptable Range. + + + Percentage of EASE2 grid cells with soil moisture measures that fall outside of a predefined acceptable range. + + + directInternal + + + percent + + + 100. + + + + + Percentage of EASE2 grid cells that lack soil moisture retrieval values relative to the total number of grid cells where soil moisture retrieval was attempted. + + + Percent of Missing Data + + + percent + + + 124.2959595 + + + directInternal + + + + + + eng + + + utf8 + + + 1.0 + + + Product Specification Document for the SMAP Level 3 Passive Soil Moisture Product (L3_SM_P) + + + 2013-02-08 + + + L3_SM_P + + + + + doi:10.5067/4XXOGX0OOW1S + + + SPL3SMP + + + SMAP + + + utf8 + + + National Aeronautics and Space Administration (NASA) + + + eng + + + 009 + + + Daily global composite of up-to 30 half-orbit L2_SM_P soil moisture estimates based on radiometer brightness temperature measurements acquired by the SMAP radiometer during ascending and descending half-orbits at approximately 6 PM and 6 AM local solar time. + + + onGoing + + + 2023-08-31 + + + The software that generates the Level 3 Soil Moisture Passive product and the data system that automates its production were designed and implemented + at the Jet Propulsion Laboratory, California Institute of Technology in Pasadena, California. + + + geoscientificInformation + + + The SMAP L3_SM_P algorithm provides daily global composite of soil moistures based on radiometer data on a 36 km grid. + + + SMAP L3 Radiometer Global Daily 36 km EASE-Grid Soil Moisture + + + National Snow and Ice Data Center + + + R19 + + + grid + + + The Calibration and Validation Version 2 Release of the SMAP Level 3 Daily Global Composite Passive Soil Moisture Science Processing Software. + + + + + SPL3SMP + + + utf8 + + + 026761d0-5866-418e-9acd-c1fcd308dc13 + + + eng + + + 1.8.13 + + + 009 + + + Daily global composite of up-to 15 half-orbit L2_SM_P soil moisture estimates based on radiometer brightness temperature measurements acquired by the SMAP radiometer during descending half-orbits at approximately 6 AM local solar time. + + + 2024-03-20 + + + onGoing + + + The software that generates the Level 3 SM_P product and the data system that automates its production were designed and implemented at the Jet Propulsion Laboratory, California Institute of Technology in Pasadena, California. + + + geoscientificInformation + + + The SMAP L3_SM_P effort provides soil moistures based on radiometer data on a 36 km grid. + + + HDF5 + + + SMAP_L3_SM_P_20150410_R19240_001.h5 + + + asNeeded + + + R19240 + + + Jet Propulsion Laboratory + + + 2016-05-01 + + + L3_SM_P + + + grid + + + The Calibration and Validation Version 2 Release of the SMAP Level 3 Daily Global Composite Passive Soil Moisture Science Processing Software. + + + + + Soil moisture is retrieved over land targets on the descending (AM) SMAP half-orbits when the SMAP spacecraft is travelling from North to South, while the SMAP instruments are operating in the nominal mode. The L3_SM_P product represents soil moisture retrieved over the entre UTC day. Retrievals are performed but flagged as questionable over urban areas, mountainous areas with high elevation variability, and areas with high ( &gt; 5 kg/m**2) vegetation water content; for retrievals using the high-resolution radar, cells in the nadir region are also flagged. Retrievals are inhibited for permanent snow/ice, frozen ground, and excessive static or transient open water in the cell, and for excessive RFI in the sensor data. + + + 180. + + + -180. + + + -85.04450226 + + + 85.04450226 + + + 2015-04-10T00:00:00.000Z + + + 2015-04-10T23:59:59.999Z + + + + + SMAP_L3_SM_P_20150410_R19240_001.qa + + + An ASCII product that contains statistical information on data product results. These statistics enable data producers and users to assess the quality of the data in the data product granule. + + + 2024-03-20 + + + + + 0 + + + 0 + + + 0 + + + 2 + + + SMAP Fixed Earth Grids, SMAP Science Document no: 033, May 11, 2009 + + + point + + + + 406 + + + 36. + + + + + EASE-Grid 2.0 + + + EASE-Grid 2.0: Incremental but Significant Improvements for Earth-Gridded Data Sets (ISPRS Int. J. Geo-Inf. 2012, 1, 32-45; doi:10.3390/ijgi1010032) + + + 2012-03-31 + + + + + 964 + + + 36. + + + + + The Equal-Area Scalable Earth Grid (EASE-Grid 2.0) is used for gridding satellite data sets. The EASE-Grid 2.0 is defined on the WGS84 ellipsoid to allow users to import data into standard GIS software formats such as GeoTIFF without reprojection. + + + Equal-Area Scalable Earth Grid + + + + + + 1015 + + + 1001 + + + + + + A configuration file that specifies the complete set of elements within the input Level 2 SM_P Product that the Radiometer Level 3_SM_P Science Processing Software (SPS) needs in order to function. + + + R19240 + + + SMAP_L3_SM_P_SPS_InputConfig_L2_SM_P.xml + + + 2024-03-20 + + + + + Precomputed longitude at each EASEGrid cell on 36-km grid on Global Cylindrical Projection + + + 002 + + + EZ2Lon_M36_002.float32 + + + 2013-05-09 + + + + + Passive soil moisture estimates onto a 36-km global Earth-fixed grid, based on radiometer measurements acquired when the SMAP spacecraft is travelling from North to South at approximately 6:00 AM local time. + + + SMAP_L2_SM_P_01001_A_20150409T233959_R19240_001.h5 + SMAP_L2_SM_P_01001_D_20150410T002914_R19240_001.h5 + SMAP_L2_SM_P_01002_A_20150410T011829_R19240_001.h5 + SMAP_L2_SM_P_01002_D_20150410T020739_R19240_001.h5 + SMAP_L2_SM_P_01003_A_20150410T025654_R19240_001.h5 + SMAP_L2_SM_P_01003_D_20150410T034609_R19240_001.h5 + SMAP_L2_SM_P_01004_A_20150410T043524_R19240_001.h5 + SMAP_L2_SM_P_01004_D_20150410T052435_R19240_001.h5 + SMAP_L2_SM_P_01005_A_20150410T061349_R19240_001.h5 + SMAP_L2_SM_P_01005_D_20150410T070304_R19240_001.h5 + SMAP_L2_SM_P_01006_A_20150410T075215_R19240_001.h5 + SMAP_L2_SM_P_01006_D_20150410T084130_R19240_001.h5 + SMAP_L2_SM_P_01007_A_20150410T093045_R19240_001.h5 + SMAP_L2_SM_P_01007_D_20150410T101959_R19240_001.h5 + SMAP_L2_SM_P_01008_A_20150410T110910_R19240_001.h5 + SMAP_L2_SM_P_01008_D_20150410T115825_R19240_001.h5 + SMAP_L2_SM_P_01009_A_20150410T124740_R19240_001.h5 + SMAP_L2_SM_P_01009_D_20150410T133655_R19240_001.h5 + SMAP_L2_SM_P_01010_A_20150410T142605_R19240_001.h5 + SMAP_L2_SM_P_01010_D_20150410T151520_R19240_001.h5 + SMAP_L2_SM_P_01011_A_20150410T160435_R19240_001.h5 + SMAP_L2_SM_P_01011_D_20150410T165346_R19240_001.h5 + SMAP_L2_SM_P_01012_A_20150410T174301_R19240_001.h5 + SMAP_L2_SM_P_01012_D_20150410T183216_R19240_001.h5 + SMAP_L2_SM_P_01013_A_20150410T192130_R19240_001.h5 + SMAP_L2_SM_P_01013_D_20150410T201041_R19240_001.h5 + SMAP_L2_SM_P_01014_A_20150410T205956_R19240_001.h5 + SMAP_L2_SM_P_01014_D_20150410T214911_R19240_001.h5 + SMAP_L2_SM_P_01015_A_20150410T223821_R19240_001.h5 + SMAP_L2_SM_P_01015_D_20150410T232736_R19240_001.h5 + + + doi:10.5067/K7Y2D8QQVZ4L + + + L2_SM_P + + + 2024-03-20 + 2024-03-20 + 2024-03-20 + 2024-03-20 + 2024-03-20 + 2024-03-20 + 2024-03-20 + 2024-03-20 + 2024-03-20 + 2024-03-20 + 2024-03-20 + 2024-03-20 + 2024-03-20 + 2024-03-20 + 2024-03-20 + 2024-03-20 + 2024-03-20 + 2024-03-20 + 2024-03-20 + 2024-03-20 + 2024-03-20 + 2024-03-20 + 2024-03-20 + 2024-03-20 + 2024-03-20 + 2024-03-20 + 2024-03-20 + 2024-03-20 + 2024-03-20 + 2024-03-20 + + + 36. + + + + + A configuration file that specifies the source of the values for each of the data elements that comprise the metadata in the output Radiometer Level 3_SM_P product. + + + R19240 + + + SMAP_L3_SM_P_SPS_MetConfig_L2_SM_P.xml + + + 2024-03-20 + + + + + A configuration file that lists the entire content of the output Radiometer Level 3_SM_P_ product. + + + R19240 + + + SMAP_L3_SM_P_SPS_OutputConfig_L3_SM_P.xml + + + 2024-03-20 + + + + + A configuration file generated automatically within the SMAP data system that specifies all of the conditions required for each individual run of the Radiometer Level 3 SM P Science Processing Software (SPS). + + + R19240 + + + SMAP_L3_SM_P_SPS_RunConfig_20240320T172135232.xml + + + 2024-03-20 + + + + + + Soil moisture retrieved using default retrieval algorithm from brightness temperatures acquired by the SMAP radiometer during the spacecraft descending pass. Level 2 granule data are then mosaicked on a daily basis to form the Level 3 product. + + + 2024-03-20T17:21:35.232Z + + + Algorithm Theoretical Basis Document: SMAP L2 and L3 Radiometer Soil Moisture (Passive) Data Products: L2_SM_P &amp; L3_SM_P + + + 2000-01-01T11:58:55.816Z + + + 023 + + + L3_SM_P_SPS + + + 2021-08-17 + + + 015 + + + L3_SM_P_SPS + + + Version 1.1 + + + 2019-04-15 + + + 2015-10-30 + + + 026 + + + Level 3 soil moisture product is formed by mosaicking Level 2 soil moisture granule data acquired over one day. + + + Algorithm Theoretical Basis Document: SMAP L2 and L3 Radiometer Soil Moisture (Passive) Data Products: L2_SM_P &amp; L3_SM_P + + + 2451545. + + + J2000 + + + 2012-10-26 + + + Soil Moisture Active Passive Mission (SMAP) Science Data System (SDS) Operations Facility + + + Soil Moisture Active Passive (SMAP) Radiometer processing algorithm + + + Preliminary + + + + + + + + + Longitude of the center of the Earth based grid cell. + + + degrees_east + + + + + + + Latitude of the center of the Earth based grid cell. + + + degrees_north + + + + + + + 0. + + + The fraction of the area of the 36 km grid cell that is covered by static water based on a Digital Elevation Map. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 1. + + + -9999. + + + + + + + + + Representative SCA-V soil moisture measurement for the Earth based grid cell. + + + cm**3/cm**3 + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0.01999999955 + + + 0.5 + + + -9999. + + + + + + + + + Representative angle between the antenna boresight vector and the normal to the Earth&apos;s surface for all footprints within the cell. + + + degrees + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 90. + + + -9999. + + + + + + + + + Arithmetic average of the acquisition time of all of the brightness temperature footprints with a center that falls within the EASE grid cell in UTC. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + + + + + + + Bit flags that record the conditions and the quality of the DCA retrieval algorithms that generate soil moisture for the grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 65534 + + + 1s, 2s, 4s, 8s + + + Retrieval_recommended Retrieval_attempted Retrieval_success FT_retrieval_success + + + + + + + + + The measured opacity of the vegetation used in the DCA retrieval in the grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + -999999.875 + + + 999999.875 + + + -9999. + + + + + + + + + Bit flags that represent the quality of the horizontal polarization brightness temperature within each grid cell + + + 65534 + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 1s, 2s, 4s, 8s, 16s, 32s, 64s, 128s, 256s, 512s, 1024s, 2048s, 4096s, 8192s, 16384s, 32768s + + + Horizontal_polarization_quality Horizontal_polarization_range Horizontal_polarization_RFI_detection Horizontal_polarization_RFI_correction Horizontal_polarization_NEDT Horizontal_polarization_direct_sun_correction Horizontal_polarization_reflected_sun_correction Horizontal_polarization_reflected_moon_correction Horizontal_polarization_direct_galaxy_correction Horizontal_polarization_reflected_galaxy_correction Horizontal_polarization_atmosphere_correction Horizontal_polarization_Faraday_rotation_correction Horizontal_polarization_null_value_bit Horizontal_polarization_water_correction Horizontal_polarization_RFI_check Horizontal_polarization_RFI_clean + + + + + + + + + A unitless value that is indicative of bare soil roughness used in DCA within the 36 km grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 1. + + + -9999. + + + + + + + + + + 254 + + + An enumerated type that specifies the most common landcover class in the grid cell based on the IGBP landcover map. The array order is longitude (ascending), followed by latitude (descending), and followed by IGBP land cover type descending dominance (only the first three types are listed) + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + + + + + + + The row index of the 36 km EASE grid cell that contains the associated data. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0 + + + 405 + + + 65534 + + + + + + + + + Horizontal polarization brightness temperature in 36 km Earth grid cell before adjustment for the presence of water bodies. + + + Kelvin + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 330. + + + -9999. + + + + + + + + + Vertical polarization brightness temperature in 36 km Earth grid cell adjusted for the presence of water bodies. + + + Kelvin + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 330. + + + -9999. + + + + + + + + + Fourth stokes parameter for each 36 km grid cell calculated with an adjustment for the presence of water bodies + + + Kelvin + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 330. + + + -9999. + + + + + + + + + Weighted average of the longitude of the center of the brightness temperature footprints that fall within the EASE grid cell. + + + degrees + + + -180. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 179.9989929 + + + -9999. + + + + + + + + + The measured opacity of the vegetation used in the SCA-H retrieval in the grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + -999999.875 + + + 999999.875 + + + -9999. + + + + + + + + + Bit flags that record the conditions and the quality of the SCA-H retrieval algorithms that generate soil moisture for the grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 65534 + + + 1s, 2s, 4s, 8s + + + Retrieval_recommended Retrieval_attempted Retrieval_success FT_retrieval_success + + + + + + + + + A unitless value that is indicative of bare soil roughness used in DCA within the 36 km grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 1. + + + -9999. + + + /Soil_Moisture_Retrieval_Data_AM/roughness_coefficient + + + + + + + + + Diffuse reflecting power of the Earth&apos;s surface used in SCA-H within the grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 1. + + + -9999. + + + + + + + + + + 0. + + + The fraction of the grid cell that contains the most common land cover in that area based on the IGBP landcover map. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 1. + + + -9999. + + + + + + + + + 0. + + + Diffuse reflecting power of the Earth&apos;s surface used in SCA-V within the grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 1. + + + -9999. + + + + + + + + + Bit flags that record the conditions and the quality of the DCA retrieval algorithms that generate soil moisture for the grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 65534 + + + 1s, 2s, 4s, 8s + + + Retrieval_recommended Retrieval_attempted Retrieval_success FT_retrieval_success + + + /Soil_Moisture_Retrieval_Data_AM/retrieval_qual_flag_dca + + + + + + + + + Representative measure of water in the vegetation within the 36 km grid cell. + + + kg/m**2 + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 20. + + + -9999. + + + + + + + + + Vertical polarization brightness temperature in 36 km Earth grid cell before adjustment for the presence of water bodies. + + + Kelvin + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 330. + + + -9999. + + + + + + + + + A unitless value that is indicative of bare soil roughness used in SCA-V within the 36 km grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 1. + + + -9999. + + + + + + + + + Representative SCA-H soil moisture measurement for the Earth based grid cell. + + + cm**3/cm**3 + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0.01999999955 + + + 0.5 + + + -9999. + + + + + + + + + Gain weighted fraction of static water within the radiometer horizontal polarization brightness temperature antenna pattern in 36 km Earth grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 1. + + + -9999. + + + + + + + + + A unitless value that is indicative of bare soil roughness used in SCA-H within the 36 km grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 1. + + + -9999. + + + + + + + + + Representative DCA soil moisture measurement for the Earth based grid cell. + + + cm**3/cm**3 + + + 0.01999999955 + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0.5 + + + -9999. + + + + + + + + + Third stokes parameter for each 36 km grid cell calculated with an adjustment for the presence of water bodies + + + Kelvin + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 330. + + + -9999. + + + + + + + + + Bit flags that represent the quality of the 3rd Stokes brightness temperature within each grid cell + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 65534 + + + 1s, 2s, 4s, 8s, 16s, 32s, 64s, 128s, 256s, 512s, 1024s, 4096s, 16384s, 32768s + + + 3rd_Stokes_quality 3rd_Stokes_range 3rd_Stokes_RFI_detection 3rd_Stokes_RFI_correction 3rd_Stokes_NEDT 3rd_Stokes_direct_sun_correction 3rd_Stokes_reflected_sun_correction 3rd_Stokes_reflected_moon_correction 3rd_Stokes_direct_galaxy_correction 3rd_Stokes_reflected_galaxy_correction 3rd_Stokes_atmosphere_correction 3rd_Stokes_null_value_bit 3rd_Stokes_RFI_check 3rd_Stokes_RFI_clean + + + + + + + + + Bit flags that represent the quality of the 4th Stokes brightness temperature within each grid cell + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 65534 + + + 1s, 2s, 4s, 8s, 16s, 32s, 64s, 128s, 256s, 512s, 1024s, 4096s, 16384s, 32768s + + + 4th_Stokes_quality 4th_Stokes_range 4th_Stokes_RFI_detection 4th_Stokes_RFI_correction 4th_Stokes_NEDT 4th_Stokes_direct_sun_correction 4th_Stokes_reflected_sun_correction 4th_Stokes_reflected_moon_correction 4th_Stokes_direct_galaxy_correction 4th_Stokes_reflected_galaxy_correction 4th_Stokes_atmosphere_correction 4th_Stokes_null_value_bit 4th_Stokes_RFI_check 4th_Stokes_RFI_clean + + + + + + + + + Horizontal polarization brightness temperature in 36 km Earth grid cell adjusted for the presence of water bodies. + + + Kelvin + + + 0. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 330. + + + -9999. + + + + + + + + + Gain weighted fraction of static water within the radiometer vertical polarization brightness temperature antenna pattern in 36 km Earth grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 1. + + + -9999. + + + + + + + + + Diffuse reflecting power of the Earth&apos;s surface used in DCA within the grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 1. + + + -9999. + + + + + + + + + Indicates if the grid point lies on land (0) or water (1). + + + 65534 + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0 + + + 1 + + + + + + + + + Arithmetic average of the acquisition time of all of the brightness temperature footprints with a center that falls within the EASE grid cell in seconds since noon on January 1, 2000 UTC. + + + seconds + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + -999999.90000000002 + + + 940000000. + + + -9999. + + + + + + + + + The measured opacity of the vegetation used in the DCA retrieval in the grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + -999999.875 + + + 999999.875 + + + -9999. + + + /Soil_Moisture_Retrieval_Data_AM/vegetation_opacity + + + + + + + + + A unitless value that is indicative of aggregated bulk_density within the 36 km grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 2.650000095 + + + -9999. + + + + + + + + + Net uncertainty measure of soil moisture measure for the Earth based grid cell. - Calculation method is TBD. + + + cm**3/cm**3 + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 0.200000003 + + + -9999. + + + + + + + + + The fraction of the area of the 36 km grid cell that is covered by water based on the radar detection algorithm. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 1. + + + -9999. + + + + + + + + + Bit flags that represent the quality of the vertical polarization brightness temperature within each grid cell + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 65534 + + + 1s, 2s, 4s, 8s, 16s, 32s, 64s, 128s, 256s, 512s, 1024s, 2048s, 4096s, 8192s, 16384s, 32768s + + + Vertical_polarization_quality Vertical_polarization_range Vertical_polarization_RFI_detection Vertical_polarization_RFI_correction Vertical_polarization_NEDT Vertical_polarization_direct_sun_correction Vertical_polarization_reflected_sun_correction Vertical_polarization_reflected_moon_correction Vertical_polarization_direct_galaxy_correction Vertical_polarization_reflected_galaxy_correction Vertical_polarization_atmosphere_correction Vertical_polarization_Faraday_rotation_correction Vertical_polarization_null_value_bit Vertical_polarization_water_correction Vertical_polarization_RFI_check Vertical_polarization_RFI_clean + + + + + + + + + Weighted average of the latitude of the center of the brightness temperature footprints that fall within the EASE grid cell. + + + degrees + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + -90. + + + 90. + + + -9999. + + + + + + + + + The column index of the 36 km EASE grid cell that contains the associated data. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0 + + + 963 + + + 65534 + + + + + + + + + Temperature at land surface based on GMAO GEOS-5 data. + + + Kelvins + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 350. + + + -9999. + + + + + + + + + Representative DCA soil moisture measurement for the Earth based grid cell. + + + cm**3/cm**3 + + + 0.01999999955 + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0.5 + + + -9999. + + + /Soil_Moisture_Retrieval_Data_AM/soil_moisture + + + + + + + + + Bit flags that record the conditions and the quality of the SCA-V retrieval algorithms that generate soil moisture for the grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 65534 + + + 1s, 2s, 4s, 8s + + + Retrieval_recommended Retrieval_attempted Retrieval_success FT_retrieval_success + + + + + + + + + Fraction of the 36 km grid cell that is denoted as frozen. Based on binary flag that specifies freeze thaw conditions in each of the component 3 km grid cells. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 1. + + + -9999. + + + + + + + + + Bit flags that record ambient surface conditions for the grid cell + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 65534 + + + 1s, 2s, 4s, 8s, 16s, 32s, 64s, 128s, 256s, 512s, 1024s, 2048s + + + 36_km_static_water_body 36_km_radar_water_body_detection 36_km_coastal_proximity 36_km_urban_area 36_km_precipitation 36_km_snow_or_ice 36_km_permanent_snow_or_ice 36_km_radiometer_frozen_ground 36_km_model_frozen_ground 36_km_mountainous_terrain 36_km_dense_vegetation 36_km_nadir_region + + + + + + + + + The measured opacity of the vegetation used in the SCA-V retrieval in the grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + -999999.875 + + + 999999.875 + + + -9999. + + + + + + + + + Diffuse reflecting power of the Earth&apos;s surface used in DCA within the grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 1. + + + -9999. + + + /Soil_Moisture_Retrieval_Data_AM/albedo + + + + + + + + + A unitless value that is indicative of aggregated clay fraction within the 36 km grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 1. + + + -9999. + + + + + + + + + + + A unitless value that is indicative of aggregated bulk density within the 36 km grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 2.650000095 + + + -9999. + + + + + + + Representative angle between the antenna boresight vector and the normal to the Earth&apos;s surface for all footprints within the cell. + + + degrees + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 90. + + + -9999. + + + + + + + The row index of the 36 km EASE grid cell that contains the associated data. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0 + + + 405 + + + 65534 + + + + + + + The fraction of the area of the 36 km grid cell that is covered by static water based on a Digital Elevation Map. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 1. + + + -9999. + + + + + + + Fraction of the 36 km grid cell that is denoted as frozen. Based on binary flag that specifies freeze thaw conditions in each of the component 3 km grid cells. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 1. + + + -9999. + + + + + + + A unitless value that is indicative of bare soil roughness used in DCA retrievals within the 36 km grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 1. + + + -9999. + + + + + + + A unitless value that is indicative of bare soil roughness used in DCA retrievals within the 36 km grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 1. + + + -9999. + + + /Soil_Moisture_Retrieval_Data_PM/roughness_coefficient_dca_pm + + + + + + + Bit flags that record ambient surface conditions for the grid cell + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 65534 + + + 1s, 2s, 4s, 8s, 16s, 32s, 64s, 128s, 256s, 512s, 1024s, 2048s + + + 36_km_static_water_body 36_km_radar_water_body_detection 36_km_coastal_proximity 36_km_urban_area 36_km_precipitation 36_km_snow_or_ice 36_km_permanent_snow_or_ice 36_km_radar_frozen_ground 36_km_model_frozen_ground 36_km_mountainous_terrain 36_km_dense_vegetation 36_km_nadir_region + + + + + + + 65534 + + + 1s, 2s, 4s, 8s + + + Bit flags that record the conditions and the quality of the DCA retrieval algorithms that generate soil moisture for the grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + Retrieval_recommended Retrieval_attempted Retrieval_success FT_retrieval_success + + + + + + + Horizontal polarization brightness temperature in 36 km Earth grid cell adjusted for the presence of water bodies. + + + Kelvin + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 330. + + + -9999. + + + + + + + 65534 + + + 1s, 2s, 4s, 8s + + + Bit flags that record the conditions and the quality of the SCA-V retrieval algorithms that generate soil moisture for the grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + Retrieval_recommended Retrieval_attempted Retrieval_success FT_retrieval_success + + + + + + + Representative DCA soil moisture measurement for the Earth based grid cell. + + + cm**3/cm**3 + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0.01999999955 + + + 0.5 + + + -9999. + + + + + + + Diffuse reflecting power of the Earth&apos;s surface used in SCA-V retrievals within the grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 1. + + + -9999. + + + + + + + 0. + + + -9999. + + + Gain weighted fraction of static water within the radiometer vertical polarization brightness temperature antenna pattern in 36 km Earth grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 1. + + + + + + + A unitless value that is indicative of bare soil roughness used in SCA-V retrievals within the 36 km grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 1. + + + -9999. + + + + + + + 65534 + + + Bit flags that represent the quality of the 4th Stokes brightness temperature within each grid cell + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 1s, 2s, 4s, 8s, 16s, 32s, 64s, 128s, 256s, 512s, 1024s, 4096s, 16384s, 32768s + + + 4th_Stokes_quality 4th_Stokes_range 4th_Stokes_RFI_detection 4th_Stokes_RFI_correction 4th_Stokes_NEDT 4th_Stokes_direct_sun_correction 4th_Stokes_reflected_sun_correction 4th_Stokes_reflected_moon_correction 4th_Stokes_direct_galaxy_correction 4th_Stokes_reflected_galaxy_correction 4th_Stokes_atmosphere_correction 4th_Stokes_null_value_bit 4th_Stokes_RFI_check 4th_Stokes_RFI_clean + + + + + + + Third stokes parameter for each 36 km grid cell calculated with an adjustment for the presence of water bodies + + + Kelvin + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 330. + + + -9999. + + + + + + + 65534 + + + 1s, 2s, 4s, 8s + + + Bit flags that record the conditions and the quality of the DCA retrieval algorithms that generate soil moisture for the grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + Retrieval_recommended Retrieval_attempted Retrieval_success FT_retrieval_success + + + /Soil_Moisture_Retrieval_Data_PM/retrieval_qual_flag_pm + + + + + + + Representative SCA-V soil moisture measurement for the Earth based grid cell. + + + cm**3/cm**3 + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0.01999999955 + + + 0.5 + + + -9999. + + + + + + + 65534 + + + 1s, 2s, 4s, 8s + + + Bit flags that record the conditions and the quality of the SCA-H retrieval algorithms that generate soil moisture for the grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + Retrieval_recommended Retrieval_attempted Retrieval_success FT_retrieval_success + + + + + + + The measured opacity of the vegetation used in SCA-H retrievals in the grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + -999999.875 + + + 999999.875 + + + -9999. + + + + + + + Kelvin + + + 0. + + + Vertical polarization brightness temperature in 36 km Earth grid cell before adjustment for the presence of water bodies. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 330. + + + -9999. + + + + + + + A unitless value that is indicative of bare soil roughness used in SCA-H retrievals within the 36 km grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 1. + + + -9999. + + + + + + + Bit flags that represent the quality of the horizontal polarization brightness temperature within each grid cell + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 65534 + + + 1s, 2s, 4s, 8s, 16s, 32s, 64s, 128s, 256s, 512s, 1024s, 2048s, 4096s, 8192s, 16384s, 32768s + + + Horizontal_polarization_quality Horizontal_polarization_range Horizontal_polarization_RFI_detection Horizontal_polarization_RFI_correction Horizontal_polarization_NEDT Horizontal_polarization_direct_sun_correction Horizontal_polarization_reflected_sun_correction Horizontal_polarization_reflected_moon_correction Horizontal_polarization_direct_galaxy_correction Horizontal_polarization_reflected_galaxy_correction Horizontal_polarization_atmosphere_correction Horizontal_polarization_Faraday_rotation_correction Horizontal_polarization_null_value_bit Horizontal_polarization_water_correction Horizontal_polarization_RFI_check Horizontal_polarization_RFI_clean + + + + + + + A unitless value that is indicative of aggregated clay fraction within the 36 km grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 1. + + + -9999. + + + + + + + Weighted average of the latitude of the center of the brightness temperature footprints that fall within the EASE grid cell. + + + degrees + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + -90. + + + 90. + + + -9999. + + + + + + + The column index of the 36 km EASE grid cell that contains the associated data. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0 + + + 963 + + + 65534 + + + + + + + Arithmetic average of the acquisition time of all of the brightness temperature footprints with a center that falls within the EASE grid cell in seconds since noon on January 1, 2000 UTC. + + + seconds + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + -999999.90000000002 + + + 940000000. + + + -9999. + + + + + + + Fourth stokes parameter for each 36 km grid cell calculated with an adjustment for the presence of water bodies + + + Kelvin + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 330. + + + -9999. + + + + + + + Net uncertainty measure of soil moisture measure for the Earth based grid cell. - Calculation method is TBD. + + + cm**3/cm**3 + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 0.200000003 + + + -9999. + + + + + + + + 254 + + + An enumerated type that specifies the most common landcover class in the grid cell based on the IGBP landcover map. The array order is longitude (ascending), followed by latitude (descending), and followed by IGBP land cover type descending dominance (only the first three types are listed) + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + + + + + Arithmetic average of the acquisition time of all of the brightness temperature footprints with a center that falls within the EASE grid cell in UTC. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + + + + + Diffuse reflecting power of the Earth&apos;s surface used in DCA retrievals within the grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 1. + + + -9999. + + + + + + + Longitude of the center of the Earth based grid cell. + + + degrees_east + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + -180. + + + 179.9989929 + + + -9999. + + + + + + + 65534 + + + Bit flags that represent the quality of the 3rd Stokes brightness temperature within each grid cell + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 1s, 2s, 4s, 8s, 16s, 32s, 64s, 128s, 256s, 512s, 1024s, 4096s, 16384s, 32768s + + + 3rd_Stokes_quality 3rd_Stokes_range 3rd_Stokes_RFI_detection 3rd_Stokes_RFI_correction 3rd_Stokes_NEDT 3rd_Stokes_direct_sun_correction 3rd_Stokes_reflected_sun_correction 3rd_Stokes_reflected_moon_correction 3rd_Stokes_direct_galaxy_correction 3rd_Stokes_reflected_galaxy_correction 3rd_Stokes_atmosphere_correction 3rd_Stokes_null_value_bit 3rd_Stokes_RFI_check 3rd_Stokes_RFI_clean + + + + + + + The measured opacity of the vegetation used in DCA retrievals in the grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + -999999.875 + + + 999999.875 + + + -9999. + + + + + + + Bit flags that represent the quality of the vertical polarization brightness temperature within each grid cell + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 65534 + + + 1s, 2s, 4s, 8s, 16s, 32s, 64s, 128s, 256s, 512s, 1024s, 2048s, 4096s, 8192s, 16384s, 32768s + + + Vertical_polarization_quality Vertical_polarization_range Vertical_polarization_RFI_detection Vertical_polarization_RFI_correction Vertical_polarization_NEDT Vertical_polarization_direct_sun_correction Vertical_polarization_reflected_sun_correction Vertical_polarization_reflected_moon_correction Vertical_polarization_direct_galaxy_correction Vertical_polarization_reflected_galaxy_correction Vertical_polarization_atmosphere_correction Vertical_polarization_Faraday_rotation_correction Vertical_polarization_null_value_bit Vertical_polarization_water_correction Vertical_polarization_RFI_check Vertical_polarization_RFI_clean + + + + + + + 0. + + + The fraction of the area of the 36 km grid cell that is covered by water based on the radar detection algorithm. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 1. + + + -9999. + + + + + + + Representative SCA-H soil moisture measurement for the Earth based grid cell. + + + cm**3/cm**3 + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0.01999999955 + + + 0.5 + + + -9999. + + + + + + + Representative DCA soil moisture measurement for the Earth based grid cell. + + + cm**3/cm**3 + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0.01999999955 + + + 0.5 + + + -9999. + + + /Soil_Moisture_Retrieval_Data_PM/soil_moisture_pm + + + + + + + + 0. + + + The fraction of the grid cell that contains the most common land cover in that area based on the IGBP landcover map. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 1. + + + -9999. + + + + + + + The measured opacity of the vegetation used in DCA retrievals in the grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + -999999.875 + + + 999999.875 + + + -9999. + + + /Soil_Moisture_Retrieval_Data_PM/vegetation_opacity_dca_pm + + + + + + + 0. + + + Gain weighted fraction of static water within the radiometer horizontal polarization brightness temperature antenna pattern in 36 km Earth grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 1. + + + -9999. + + + + + + + Weighted average of the longitude of the center of the brightness temperature footprints that fall within the EASE grid cell. + + + degrees + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + -180. + + + 179.9989929 + + + -9999. + + + + + + + Diffuse reflecting power of the Earth&apos;s surface used in DCA retrievals within the grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 1. + + + -9999. + + + /Soil_Moisture_Retrieval_Data_PM/albedo_dca_pm + + + + + + + Representative measure of water in the vegetation within the 36 km grid cell. + + + kg/m**2 + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 20. + + + -9999. + + + + + + + 0. + + + Diffuse reflecting power of the Earth&apos;s surface used in SCA-H retrievals within the grid cell. + + + 1. + + + -9999. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + + + + + Horizontal polarization brightness temperature in 36 km Earth grid cell before adjustment for the presence of water bodies. + + + Kelvin + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 330. + + + -9999. + + + + + + + Latitude of the center of the Earth based grid cell. + + + degrees_north + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + -90. + + + 90. + + + -9999. + + + + + + + Vertical polarization brightness temperature in 36 km Earth grid cell adjusted for the presence of water bodies. + + + Kelvin + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 330. + + + -9999. + + + + + + + The measured opacity of the vegetation used in SCA-V retrievals in the grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + -999999.875 + + + 999999.875 + + + -9999. + + + + + + + Indicates if the grid point lies on land (0) or water (1). + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0 + + + 1 + + + 65534 + + + + + + + Temperature at land surface based on GMAO GEOS-5 data. + + + Kelvins + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 350. + + + -9999. + + + + diff --git a/tests/unit/test_projection_utilities.py b/tests/unit/test_projection_utilities.py index 0da8f66..fe5b183 100644 --- a/tests/unit/test_projection_utilities.py +++ b/tests/unit/test_projection_utilities.py @@ -194,6 +194,26 @@ def test_get_variable_crs(self): 'present in granule .dmr file.', ) + with self.subTest('grid_mapping override with json configuration'): + smap_varinfo = VarInfoFromDmr( + 'tests/data/SC_SPL3SMP_009.dmr', + 'SPL3SMP', + 'hoss/hoss_config.json', + ) + expected_crs = CRS.from_cf( + { + 'false_easting': 0.0, + 'false_northing': 0.0, + 'longitude_of_central_meridian': 0.0, + 'standard_parallel': 30.0, + 'grid_mapping_name': 'lambert_cylindrical_equal_area', + } + ) + actual_crs = get_variable_crs( + '/Soil_Moisture_Retrieval_Data_AM/surface_flag', smap_varinfo + ) + self.assertEqual(actual_crs, expected_crs) + def test_get_projected_x_y_extents(self): """Ensure that the expected values for the x and y dimension extents are recovered for a known projected grid and requested input. From 1e7bc3528f939b237993c87f1328dfba49f2437d Mon Sep 17 00:00:00 2001 From: sudhamurthy Date: Thu, 3 Oct 2024 22:18:14 -0400 Subject: [PATCH 30/41] DAS-2232 - added a module to contain coordinate methods --- hoss/coordinate_utilities.py | 354 +++++++++++++++++++++++++++++++++++ hoss/dimension_utilities.py | 334 +-------------------------------- hoss/spatial.py | 8 +- 3 files changed, 363 insertions(+), 333 deletions(-) create mode 100644 hoss/coordinate_utilities.py diff --git a/hoss/coordinate_utilities.py b/hoss/coordinate_utilities.py new file mode 100644 index 0000000..f1cd0fd --- /dev/null +++ b/hoss/coordinate_utilities.py @@ -0,0 +1,354 @@ +""" This module contains utility functions used for + coordinate variables and methods to convert the + coordinate variable data to x/y dimension scales +""" + +from typing import Dict, Set, Tuple + +import numpy as np +from netCDF4 import Dataset +from numpy import ndarray +from numpy.ma.core import MaskedArray +from pyproj import CRS +from varinfo import VariableFromDmr, VarInfoFromDmr + +from hoss.exceptions import ( + IrregularCoordinateDatasets, + MissingCoordinateDataset, + MissingValidCoordinateDataset, +) +from hoss.projection_utilities import ( + get_x_y_extents_from_geographic_points, +) + + +def get_override_projected_dimension_name( + varinfo: VarInfoFromDmr, + variable_name: str, +) -> str: + """returns the x-y projection variable names that would + match the geo coordinate names. The `latitude` coordinate + variable name gets converted to 'projected_y' dimension scale + and the `longitude` coordinate variable name gets converted to + 'projected_x' + + """ + override_variable = varinfo.get_variable(variable_name) + projected_dimension_name = '' + if override_variable is not None: + if override_variable.is_latitude(): + projected_dimension_name = 'projected_y' + elif override_variable.is_longitude(): + projected_dimension_name = 'projected_x' + return projected_dimension_name + + +def get_override_projected_dimensions( + varinfo: VarInfoFromDmr, + variable_name: str, +) -> list[str]: + """ + Returns the projected dimensions names from coordinate variables + """ + latitude_coordinates, longitude_coordinates = get_coordinate_variables( + varinfo, [variable_name] + ) + if latitude_coordinates and longitude_coordinates: + # there should be only 1 lat and lon coordinate for one variable + override_dimensions = [] + override_dimensions.append( + get_override_projected_dimension_name(varinfo, latitude_coordinates[0]) + ) + override_dimensions.append( + get_override_projected_dimension_name(varinfo, longitude_coordinates[0]) + ) + + else: + # if the override is the variable + override_projected_dimension_name = get_override_projected_dimension_name( + varinfo, variable_name + ) + override_dimensions = ['projected_y', 'projected_x'] + if override_projected_dimension_name not in override_dimensions: + override_dimensions = [] + return override_dimensions + + +def get_variables_with_anonymous_dims( + varinfo: VarInfoFromDmr, required_variables: set[str] +) -> bool: + """ + returns the list of required variables without any + dimensions + """ + return set( + required_variable + for required_variable in required_variables + if len(varinfo.get_variable(required_variable).dimensions) == 0 + ) + + +def get_coordinate_variables( + varinfo: VarInfoFromDmr, + requested_variables: Set[str], +) -> tuple[list, list]: + """This method returns coordinate variables that are referenced + in the variables requested. It returns it in a specific order + [latitude, longitude] + """ + + coordinate_variables_set = varinfo.get_references_for_attribute( + requested_variables, 'coordinates' + ) + + latitude_coordinate_variables = [ + coordinate + for coordinate in coordinate_variables_set + if varinfo.get_variable(coordinate).is_latitude() + ] + + longitude_coordinate_variables = [ + coordinate + for coordinate in coordinate_variables_set + if varinfo.get_variable(coordinate).is_longitude() + ] + + return latitude_coordinate_variables, longitude_coordinate_variables + + +def update_dimension_variables( + prefetch_dataset: Dataset, + latitude_coordinate: VariableFromDmr, + longitude_coordinate: VariableFromDmr, + crs: CRS, +) -> Dict[str, ndarray]: + """Generate artificial 1D dimensions variable for each + 2D dimension or coordinate variable + + For each dimension variable: + (1) Check if the dimension variable is 1D. + (2) If it is not 1D and is 2D get the dimension sizes + (3) Get the corner points from the coordinate variables + (4) Get the x-y max-min values + (5) Generate the x-y dimscale array and return to the calling method + + """ + # if there is more than one grid, determine the lat/lon coordinate + # associated with this variable + row_size, col_size = get_row_col_sizes_from_coordinate_datasets( + prefetch_dataset, + latitude_coordinate, + longitude_coordinate, + ) + + geo_grid_corners = get_geo_grid_corners( + prefetch_dataset, + latitude_coordinate, + longitude_coordinate, + ) + + x_y_extents = get_x_y_extents_from_geographic_points(geo_grid_corners, crs) + + # get grid size and resolution + x_min = x_y_extents['x_min'] + x_max = x_y_extents['x_max'] + y_min = x_y_extents['y_min'] + y_max = x_y_extents['y_max'] + x_resolution = (x_max - x_min) / row_size + y_resolution = (y_max - y_min) / col_size + + # create the xy dim scales + lat_asc, lon_asc = is_lat_lon_ascending( + prefetch_dataset, + latitude_coordinate, + longitude_coordinate, + ) + + if lon_asc: + x_dim = np.arange(x_min, x_max, x_resolution) + else: + x_dim = np.arange(x_min, x_max, -x_resolution) + + if lat_asc: + y_dim = np.arange(y_max, y_min, y_resolution) + else: + y_dim = np.arange(y_max, y_min, -y_resolution) + + return {'projected_y': y_dim, 'projected_x': x_dim} + + +def get_row_col_sizes_from_coordinate_datasets( + prefetch_dataset: Dataset, + latitude_coordinate: VariableFromDmr, + longitude_coordinate: VariableFromDmr, +) -> Tuple[int, int]: + """ + This method returns the row and column sizes of the coordinate datasets + + """ + lat_arr, lon_arr = get_lat_lon_arrays( + prefetch_dataset, latitude_coordinate, longitude_coordinate + ) + if lat_arr.ndim > 1: + col_size = lat_arr.shape[0] + row_size = lat_arr.shape[1] + if (lon_arr.shape[0] != lat_arr.shape[0]) or (lon_arr.shape[1] != lat_arr.shape[1]): + raise IrregularCoordinateDatasets(lon_arr.shape, lat_arr.shape) + if lat_arr.ndim and lon_arr.ndim == 1: + col_size = lat_arr.size + row_size = lon_arr.size + return row_size, col_size + + +def is_lat_lon_ascending( + prefetch_dataset: Dataset, + latitude_coordinate: VariableFromDmr, + longitude_coordinate: VariableFromDmr, +) -> tuple[bool, bool]: + """ + Checks if the latitude and longitude cooordinate datasets have values + that are ascending + """ + lat_arr, lon_arr = get_lat_lon_arrays( + prefetch_dataset, latitude_coordinate, longitude_coordinate + ) + lat_col = lat_arr[:, 0] + lon_row = lon_arr[0, :] + + first_index, last_index = np.ma.flatnotmasked_edges(lat_col) + latitude_ascending = lat_col.size == 1 or lat_col[first_index] < lat_col[last_index] + + first_index, last_index = np.ma.flatnotmasked_edges(lat_col) + longitude_ascending = ( + lon_row.size == 1 or lon_row[first_index] < lon_row[last_index] + ) + + return latitude_ascending, longitude_ascending + + +def get_lat_lon_arrays( + prefetch_dataset: Dataset, + latitude_coordinate: VariableFromDmr, + longitude_coordinate: VariableFromDmr, +) -> Tuple[ndarray, ndarray]: + """ + This method is used to return the lat lon arrays from a 2D + coordinate dataset. + """ + lat_arr = [] + lon_arr = [] + + lat_arr = prefetch_dataset[latitude_coordinate.full_name_path][:] + lon_arr = prefetch_dataset[longitude_coordinate.full_name_path][:] + + return lat_arr, lon_arr + + +def get_geo_grid_corners( + prefetch_dataset: Dataset, + latitude_coordinate: VariableFromDmr, + longitude_coordinate: VariableFromDmr, +) -> list[Tuple[float]]: + """ + This method is used to return the lat lon corners from a 2D + coordinate dataset. It gets the row and column of the latitude and longitude + arrays to get the corner points. This does a check for fill values and + This method does not check if there are fill values in the corner points + to go down to the next row and col. The fill values in the corner points + still needs to be addressed. It will raise an exception in those + cases. + """ + lat_arr, lon_arr = get_lat_lon_arrays( + prefetch_dataset, + latitude_coordinate, + longitude_coordinate, + ) + + if not lat_arr.size: + raise MissingCoordinateDataset('latitude') + if not lon_arr.size: + raise MissingCoordinateDataset('longitude') + + top_left_row_idx = 0 + top_left_col_idx = 0 + + lat_fill, lon_fill = get_fill_values_for_coordinates( + latitude_coordinate, longitude_coordinate + ) + + # get the first row from the longitude dataset + lon_row = lon_arr[top_left_row_idx, :] + lon_row_valid_indices = get_valid_indices(lon_row, lon_fill, 'longitude') + + # get the index of the minimum longitude after checking for invalid entries + top_left_col_idx = lon_row_valid_indices[lon_row[lon_row_valid_indices].argmin()] + min_lon = lon_row[top_left_col_idx] + + # get the index of the maximum longitude after checking for invalid entries + top_right_col_idx = lon_row_valid_indices[lon_row[lon_row_valid_indices].argmax()] + max_lon = lon_row[top_right_col_idx] + + # get the last valid longitude column to get the latitude array + lat_col = lat_arr[:, top_right_col_idx] + lat_col_valid_indices = get_valid_indices(lat_col, lat_fill, 'latitude') + + # get the index of minimum latitude after checking for valid values + bottom_right_row_idx = lat_col_valid_indices[ + lat_col[lat_col_valid_indices].argmin() + ] + min_lat = lat_col[bottom_right_row_idx] + + # get the index of maximum latitude after checking for valid values + top_right_row_idx = lat_col_valid_indices[lat_col[lat_col_valid_indices].argmax()] + max_lat = lat_col[top_right_row_idx] + + geo_grid_corners = [ + [min_lon, max_lat], + [max_lon, max_lat], + [max_lon, min_lat], + [min_lon, min_lat], + ] + return geo_grid_corners + + +def get_valid_indices( + coordinate_row_col: ndarray, coordinate_fill: float, coordinate_name: str +) -> ndarray: + """ + Returns indices of a valid array without fill values + """ + if coordinate_fill: + valid_indices = np.where(coordinate_row_col != coordinate_fill)[0] + elif coordinate_name == 'longitude': + valid_indices = np.where( + (coordinate_row_col >= -180.0) & (coordinate_row_col <= 180.0) + )[0] + elif coordinate_name == 'latitude': + valid_indices = np.where( + (coordinate_row_col >= -90.0) & (coordinate_row_col <= 90.0) + )[0] + + # if the first row does not have valid indices, + # should go down to the next row. We throw an exception + # for now till that gets addressed + if not valid_indices.size: + raise MissingValidCoordinateDataset(coordinate_name) + return valid_indices + + +def get_fill_values_for_coordinates( + latitude_coordinate: VariableFromDmr, + longitude_coordinate: VariableFromDmr, +) -> float | None: + """ + returns fill values for the variable. If it does not exist + checks for the overrides from the json file. If there is no + overrides, returns None + """ + + lat_fill_value = latitude_coordinate.get_attribute_value('_fillValue') + lon_fill_value = longitude_coordinate.get_attribute_value('_fillValue') + # if fill_value is None: + # check if there are overrides in hoss_config.json using varinfo + # else + return lat_fill_value, lon_fill_value diff --git a/hoss/dimension_utilities.py b/hoss/dimension_utilities.py index ec1512a..bb8028a 100644 --- a/hoss/dimension_utilities.py +++ b/hoss/dimension_utilities.py @@ -19,20 +19,16 @@ from harmony.message_utility import rgetattr from harmony.util import Config from netCDF4 import Dataset -from numpy import ndarray from numpy.ma.core import MaskedArray -from pyproj import CRS from varinfo import VariableFromDmr, VarInfoFromDmr +from hoss.coordinate_utilities import ( + get_coordinate_variables, + get_override_projected_dimensions, +) from hoss.exceptions import ( InvalidNamedDimension, InvalidRequestedRange, - IrregularCoordinateDatasets, - MissingCoordinateDataset, - MissingValidCoordinateDataset, -) -from hoss.projection_utilities import ( - get_x_y_extents_from_geographic_points, ) from hoss.utilities import ( format_variable_set_string, @@ -109,328 +105,6 @@ def get_prefetch_variables( return prefetch_variables_nc4 -def get_override_projected_dimension_name( - varinfo: VarInfoFromDmr, - variable_name: str, -) -> str: - """returns the x-y projection variable names that would - match the geo coordinate names. The `latitude` coordinate - variable name gets converted to 'projected_y' dimension scale - and the `longitude` coordinate variable name gets converted to - 'projected_x' - - """ - override_variable = varinfo.get_variable(variable_name) - projected_dimension_name = '' - if override_variable is not None: - if override_variable.is_latitude(): - projected_dimension_name = 'projected_y' - elif override_variable.is_longitude(): - projected_dimension_name = 'projected_x' - return projected_dimension_name - - -def get_override_projected_dimensions( - varinfo: VarInfoFromDmr, - variable_name: str, -) -> list[str]: - """ - Returns the projected dimensions names from coordinate variables - """ - latitude_coordinates, longitude_coordinates = get_coordinate_variables( - varinfo, [variable_name] - ) - if latitude_coordinates and longitude_coordinates: - # there should be only 1 lat and lon coordinate for one variable - override_dimensions = [] - override_dimensions.append( - get_override_projected_dimension_name(varinfo, latitude_coordinates[0]) - ) - override_dimensions.append( - get_override_projected_dimension_name(varinfo, longitude_coordinates[0]) - ) - - else: - # if the override is the variable - override_projected_dimension_name = get_override_projected_dimension_name( - varinfo, variable_name - ) - override_dimensions = ['projected_y', 'projected_x'] - if override_projected_dimension_name not in override_dimensions: - override_dimensions = [] - return override_dimensions - - -def get_variables_with_anonymous_dims( - varinfo: VarInfoFromDmr, required_variables: set[str] -) -> bool: - """ - returns the list of required variables without any - dimensions - """ - return set( - required_variable - for required_variable in required_variables - if len(varinfo.get_variable(required_variable).dimensions) == 0 - ) - - -def get_coordinate_variables( - varinfo: VarInfoFromDmr, - requested_variables: Set[str], -) -> tuple[list, list]: - """This method returns coordinate variables that are referenced - in the variables requested. It returns it in a specific order - [latitude, longitude] - """ - - coordinate_variables_set = varinfo.get_references_for_attribute( - requested_variables, 'coordinates' - ) - - latitude_coordinate_variables = [ - coordinate - for coordinate in coordinate_variables_set - if varinfo.get_variable(coordinate).is_latitude() - ] - - longitude_coordinate_variables = [ - coordinate - for coordinate in coordinate_variables_set - if varinfo.get_variable(coordinate).is_longitude() - ] - - return latitude_coordinate_variables, longitude_coordinate_variables - - -def update_dimension_variables( - prefetch_dataset: Dataset, - latitude_coordinate: VariableFromDmr, - longitude_coordinate: VariableFromDmr, - crs: CRS, -) -> Dict[str, ndarray]: - """Generate artificial 1D dimensions variable for each - 2D dimension or coordinate variable - - For each dimension variable: - (1) Check if the dimension variable is 1D. - (2) If it is not 1D and is 2D get the dimension sizes - (3) Get the corner points from the coordinate variables - (4) Get the x-y max-min values - (5) Generate the x-y dimscale array and return to the calling method - - """ - # if there is more than one grid, determine the lat/lon coordinate - # associated with this variable - row_size, col_size = get_row_col_sizes_from_coordinate_datasets( - prefetch_dataset, - latitude_coordinate, - longitude_coordinate, - ) - - geo_grid_corners = get_geo_grid_corners( - prefetch_dataset, - latitude_coordinate, - longitude_coordinate, - ) - - x_y_extents = get_x_y_extents_from_geographic_points(geo_grid_corners, crs) - - # get grid size and resolution - x_min = x_y_extents['x_min'] - x_max = x_y_extents['x_max'] - y_min = x_y_extents['y_min'] - y_max = x_y_extents['y_max'] - x_resolution = (x_max - x_min) / row_size - y_resolution = (y_max - y_min) / col_size - - # create the xy dim scales - lat_asc, lon_asc = is_lat_lon_ascending( - prefetch_dataset, - latitude_coordinate, - longitude_coordinate, - ) - - if lon_asc: - x_dim = np.arange(x_min, x_max, x_resolution) - else: - x_dim = np.arange(x_min, x_max, -x_resolution) - - if lat_asc: - y_dim = np.arange(y_max, y_min, y_resolution) - else: - y_dim = np.arange(y_max, y_min, -y_resolution) - - return {'projected_y': y_dim, 'projected_x': x_dim} - - -def get_row_col_sizes_from_coordinate_datasets( - prefetch_dataset: Dataset, - latitude_coordinate: VariableFromDmr, - longitude_coordinate: VariableFromDmr, -) -> Tuple[int, int]: - """ - This method returns the row and column sizes of the coordinate datasets - - """ - lat_arr, lon_arr = get_lat_lon_arrays( - prefetch_dataset, latitude_coordinate, longitude_coordinate - ) - if lat_arr.ndim > 1: - col_size = lat_arr.shape[0] - row_size = lat_arr.shape[1] - if (lon_arr.shape[0] != lat_arr.shape[0]) or (lon_arr.shape[1] != lat_arr.shape[1]): - raise IrregularCoordinateDatasets(lon_arr.shape, lat_arr.shape) - if lat_arr.ndim and lon_arr.ndim == 1: - col_size = lat_arr.size - row_size = lon_arr.size - return row_size, col_size - - -def is_lat_lon_ascending( - prefetch_dataset: Dataset, - latitude_coordinate: VariableFromDmr, - longitude_coordinate: VariableFromDmr, -) -> tuple[bool, bool]: - """ - Checks if the latitude and longitude cooordinate datasets have values - that are ascending - """ - lat_arr, lon_arr = get_lat_lon_arrays( - prefetch_dataset, latitude_coordinate, longitude_coordinate - ) - lat_col = lat_arr[:, 0] - lon_row = lon_arr[0, :] - return is_dimension_ascending(lat_col), is_dimension_ascending(lon_row) - - -def get_lat_lon_arrays( - prefetch_dataset: Dataset, - latitude_coordinate: VariableFromDmr, - longitude_coordinate: VariableFromDmr, -) -> Tuple[ndarray, ndarray]: - """ - This method is used to return the lat lon arrays from a 2D - coordinate dataset. - """ - lat_arr = [] - lon_arr = [] - - lat_arr = prefetch_dataset[latitude_coordinate.full_name_path][:] - lon_arr = prefetch_dataset[longitude_coordinate.full_name_path][:] - - return lat_arr, lon_arr - - -def get_geo_grid_corners( - prefetch_dataset: Dataset, - latitude_coordinate: VariableFromDmr, - longitude_coordinate: VariableFromDmr, -) -> list[Tuple[float]]: - """ - This method is used to return the lat lon corners from a 2D - coordinate dataset. It gets the row and column of the latitude and longitude - arrays to get the corner points. This does a check for values below -180 - which could be fill values. This method does not check if there - are fill values in the corner points to go down to the next row and col - The fill values in the corner points still needs to be addressed. - """ - lat_arr, lon_arr = get_lat_lon_arrays( - prefetch_dataset, - latitude_coordinate, - longitude_coordinate, - ) - - if not lat_arr.size: - raise MissingCoordinateDataset('latitude') - if not lon_arr.size: - raise MissingCoordinateDataset('longitude') - - top_left_row_idx = 0 - top_left_col_idx = 0 - - lat_fill, lon_fill = get_fill_values_for_coordinates( - latitude_coordinate, longitude_coordinate - ) - - # get the first row from the longitude dataset - lon_row = lon_arr[top_left_row_idx, :] - lon_row_valid_indices = get_valid_indices(lon_row, lon_fill, 'longitude') - - # get the index of the minimum longitude after checking for invalid entries - top_left_col_idx = lon_row_valid_indices[lon_row[lon_row_valid_indices].argmin()] - min_lon = lon_row[top_left_col_idx] - - # get the index of the maximum longitude after checking for invalid entries - top_right_col_idx = lon_row_valid_indices[lon_row[lon_row_valid_indices].argmax()] - max_lon = lon_row[top_right_col_idx] - - # get the last valid longitude column to get the latitude array - lat_col = lat_arr[:, top_right_col_idx] - lat_col_valid_indices = get_valid_indices(lat_col, lat_fill, 'latitude') - - # get the index of minimum latitude after checking for valid values - bottom_right_row_idx = lat_col_valid_indices[ - lat_col[lat_col_valid_indices].argmin() - ] - min_lat = lat_col[bottom_right_row_idx] - - # get the index of maximum latitude after checking for valid values - top_right_row_idx = lat_col_valid_indices[lat_col[lat_col_valid_indices].argmax()] - max_lat = lat_col[top_right_row_idx] - - geo_grid_corners = [ - [min_lon, max_lat], - [max_lon, max_lat], - [max_lon, min_lat], - [min_lon, min_lat], - ] - return geo_grid_corners - - -def get_valid_indices( - coordinate_row_col: ndarray, coordinate_fill: float, coordinate_name: str -) -> ndarray: - """ - Returns indices of a valid array without fill values - """ - if coordinate_fill: - valid_indices = np.where(coordinate_row_col != coordinate_fill)[0] - elif coordinate_name == 'longitude': - valid_indices = np.where( - (coordinate_row_col >= -180.0) & (coordinate_row_col <= 180.0) - )[0] - elif coordinate_name == 'latitude': - valid_indices = np.where( - (coordinate_row_col >= -90.0) & (coordinate_row_col <= 90.0) - )[0] - - # if the first row does not have valid indices, - # should go down to the next row. We throw an exception - # for now till that gets addressed - if not valid_indices.size: - raise MissingValidCoordinateDataset(coordinate_name) - return valid_indices - - -def get_fill_values_for_coordinates( - latitude_coordinate: VariableFromDmr, - longitude_coordinate: VariableFromDmr, -) -> float | None: - """ - returns fill values for the variable. If it does not exist - checks for the overrides from the json file. If there is no - overrides, returns None - """ - - lat_fill_value = latitude_coordinate.get_attribute_value('_fillValue') - lon_fill_value = longitude_coordinate.get_attribute_value('_fillValue') - # if fill_value is None: - # check if there are overrides in hoss_config.json using varinfo - # else - return lat_fill_value, lon_fill_value - - def add_bounds_variables( dimensions_nc4: str, required_dimensions: Set[str], diff --git a/hoss/spatial.py b/hoss/spatial.py index 3c2ee2b..8cd5d8f 100644 --- a/hoss/spatial.py +++ b/hoss/spatial.py @@ -35,15 +35,17 @@ get_harmony_message_bbox, get_shape_file_geojson, ) +from hoss.coordinate_utilities import ( + get_coordinate_variables, + get_variables_with_anonymous_dims, + update_dimension_variables, +) from hoss.dimension_utilities import ( IndexRange, IndexRanges, - get_coordinate_variables, get_dimension_bounds, get_dimension_extents, get_dimension_index_range, - get_variables_with_anonymous_dims, - update_dimension_variables, ) from hoss.projection_utilities import ( get_projected_x_y_extents, From 36e15c7ba107e815461773711916a1f8bc5cd504 Mon Sep 17 00:00:00 2001 From: sudhamurthy Date: Fri, 4 Oct 2024 11:46:36 -0400 Subject: [PATCH 31/41] DAS-2232 - moved new methods to a separate file --- CHANGELOG.md | 5 +- hoss/coordinate_utilities.py | 82 +++++++++---------- tests/unit/test_spatial.py | 149 +++++++++++++++++++++++++++++++++++ 3 files changed, 188 insertions(+), 48 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb8b4b4..a10326a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,8 @@ ## v1.1.0 ### 2024-09-10 -This version of HOSS provides support for products that do not comply with CF standards like SMAP L3. -The SMAP L3 products do not have grid mapping variable to provide the projection information. -They also do not have x-y dimension scales for the projected grid. +This version of HOSS provides support for gridded products that do not contain CF-Convention +compliant grid mapping variables and 1-D dimension variables, such as SMAP L3. New methods are added to retrieve dimension scales from coordinate attributes and grid mappings, using overrides specified in the hoss_config.json configuration file. - `get_coordinate_variables` gets coordinate datasets when the dimension scales are not present in diff --git a/hoss/coordinate_utilities.py b/hoss/coordinate_utilities.py index f1cd0fd..1eb53c4 100644 --- a/hoss/coordinate_utilities.py +++ b/hoss/coordinate_utilities.py @@ -8,7 +8,6 @@ import numpy as np from netCDF4 import Dataset from numpy import ndarray -from numpy.ma.core import MaskedArray from pyproj import CRS from varinfo import VariableFromDmr, VarInfoFromDmr @@ -133,18 +132,30 @@ def update_dimension_variables( (5) Generate the x-y dimscale array and return to the calling method """ - # if there is more than one grid, determine the lat/lon coordinate - # associated with this variable - row_size, col_size = get_row_col_sizes_from_coordinate_datasets( + lat_arr, lon_arr = get_lat_lon_arrays( prefetch_dataset, latitude_coordinate, longitude_coordinate, ) + if not lat_arr.size: + raise MissingCoordinateDataset('latitude') + if not lon_arr.size: + raise MissingCoordinateDataset('longitude') + + lat_fill, lon_fill = get_fill_values_for_coordinates( + latitude_coordinate, longitude_coordinate + ) + + row_size, col_size = get_row_col_sizes_from_coordinate_datasets( + lat_arr, + lon_arr, + ) geo_grid_corners = get_geo_grid_corners( - prefetch_dataset, - latitude_coordinate, - longitude_coordinate, + lat_arr, + lon_arr, + lat_fill, + lon_fill, ) x_y_extents = get_x_y_extents_from_geographic_points(geo_grid_corners, crs) @@ -158,11 +169,7 @@ def update_dimension_variables( y_resolution = (y_max - y_min) / col_size # create the xy dim scales - lat_asc, lon_asc = is_lat_lon_ascending( - prefetch_dataset, - latitude_coordinate, - longitude_coordinate, - ) + lat_asc, lon_asc = is_lat_lon_ascending(lat_arr, lon_arr, lat_fill, lon_fill) if lon_asc: x_dim = np.arange(x_min, x_max, x_resolution) @@ -178,17 +185,14 @@ def update_dimension_variables( def get_row_col_sizes_from_coordinate_datasets( - prefetch_dataset: Dataset, - latitude_coordinate: VariableFromDmr, - longitude_coordinate: VariableFromDmr, + lat_arr: ndarray, + lon_arr: ndarray, ) -> Tuple[int, int]: """ This method returns the row and column sizes of the coordinate datasets """ - lat_arr, lon_arr = get_lat_lon_arrays( - prefetch_dataset, latitude_coordinate, longitude_coordinate - ) + if lat_arr.ndim > 1: col_size = lat_arr.shape[0] row_size = lat_arr.shape[1] @@ -201,26 +205,27 @@ def get_row_col_sizes_from_coordinate_datasets( def is_lat_lon_ascending( - prefetch_dataset: Dataset, - latitude_coordinate: VariableFromDmr, - longitude_coordinate: VariableFromDmr, + lat_arr: ndarray, + lon_arr: ndarray, + lat_fill: float, + lon_fill: float, ) -> tuple[bool, bool]: """ Checks if the latitude and longitude cooordinate datasets have values that are ascending """ - lat_arr, lon_arr = get_lat_lon_arrays( - prefetch_dataset, latitude_coordinate, longitude_coordinate - ) + lat_col = lat_arr[:, 0] lon_row = lon_arr[0, :] - first_index, last_index = np.ma.flatnotmasked_edges(lat_col) - latitude_ascending = lat_col.size == 1 or lat_col[first_index] < lat_col[last_index] + lat_col_valid_indices = get_valid_indices(lon_row, lat_fill, 'latitude') + latitude_ascending = ( + lat_col[lat_col_valid_indices[1]] > lat_col[lat_col_valid_indices[0]] + ) - first_index, last_index = np.ma.flatnotmasked_edges(lat_col) + lon_row_valid_indices = get_valid_indices(lon_row, lon_fill, 'longitude') longitude_ascending = ( - lon_row.size == 1 or lon_row[first_index] < lon_row[last_index] + lon_row[lon_row_valid_indices[1]] > lon_row[lon_row_valid_indices[0]] ) return latitude_ascending, longitude_ascending @@ -245,9 +250,10 @@ def get_lat_lon_arrays( def get_geo_grid_corners( - prefetch_dataset: Dataset, - latitude_coordinate: VariableFromDmr, - longitude_coordinate: VariableFromDmr, + lat_arr: ndarray, + lon_arr: ndarray, + lat_fill: float, + lon_fill: float, ) -> list[Tuple[float]]: """ This method is used to return the lat lon corners from a 2D @@ -258,24 +264,10 @@ def get_geo_grid_corners( still needs to be addressed. It will raise an exception in those cases. """ - lat_arr, lon_arr = get_lat_lon_arrays( - prefetch_dataset, - latitude_coordinate, - longitude_coordinate, - ) - - if not lat_arr.size: - raise MissingCoordinateDataset('latitude') - if not lon_arr.size: - raise MissingCoordinateDataset('longitude') top_left_row_idx = 0 top_left_col_idx = 0 - lat_fill, lon_fill = get_fill_values_for_coordinates( - latitude_coordinate, longitude_coordinate - ) - # get the first row from the longitude dataset lon_row = lon_arr[top_left_row_idx, :] lon_row_valid_indices = get_valid_indices(lon_row, lon_fill, 'longitude') diff --git a/tests/unit/test_spatial.py b/tests/unit/test_spatial.py index e5ee6d3..e0c72da 100644 --- a/tests/unit/test_spatial.py +++ b/tests/unit/test_spatial.py @@ -17,6 +17,7 @@ get_longitude_in_grid, get_projected_x_y_index_ranges, get_spatial_index_ranges, + get_x_y_index_ranges_from_coordinates, ) @@ -182,6 +183,154 @@ def test_get_spatial_index_ranges_geographic(self): {'/latitude': (5, 44), '/longitude': (160, 199)}, ) + @patch('hoss.spatial.get_dimension_index_range') + @patch('hoss.spatial.get_projected_x_y_extents') + def test_get_x_y_index_ranges_from_coordinates( + self, mock_get_x_y_extents, mock_get_dimension_index_range + ): + """Ensure that x and y index ranges are only requested only when there are + no projected dimensions and when there are coordinate datasets, + and the values have not already been calculated. + + The example used in this test is for the SMAP SPL3SMP collection, + (SMAP L3 Radiometer Global Daily 36 km EASE-Grid Soil Moisture) + which has a Equal-Area Scalable Earth Grid (EASE-Grid 2.0) CRS for + a projected grid which is lambert_cylindrical_equal_area projection + + """ + smap_varinfo = VarInfoFromDmr( + 'tests/data/SC_SPL3SMP_009.dmr', + 'SPL3SMP', + 'hoss/hoss_config.json', + ) + smap_file_path = 'tests/data/SC_SPL3SMP_009_prefetch.nc4' + expected_index_ranges = {'projected_x': (487, 595), 'projected_y': (9, 38)} + bbox = BBox(2, 54, 42, 72) + + latitude_coordinate = smap_varinfo.get_variable( + '/Soil_Moisture_Retrieval_Data_AM/latitude' + ) + longitude_coordinate = smap_varinfo.get_variable( + '/Soil_Moisture_Retrieval_Data_AM/longitude' + ) + + crs = CRS.from_cf( + { + 'false_easting': 0.0, + 'false_northing': 0.0, + 'longitude_of_central_meridian': 0.0, + 'standard_parallel': 30.0, + 'grid_mapping_name': 'lambert_cylindrical_equal_area', + } + ) + + x_y_extents = { + 'x_min': 192972.56050179302, + 'x_max': 4052423.7705376535, + 'y_min': 5930779.396449475, + 'y_max': 6979878.9118312765, + } + + mock_get_x_y_extents.return_value = x_y_extents + + # When ranges are derived, they are first calculated for x, then y: + mock_get_dimension_index_range.side_effect = [(487, 595), (9, 38)] + + with self.subTest( + 'Projected grid from coordinates gets expected dimension ranges' + ): + with Dataset(smap_file_path, 'r') as smap_prefetch: + self.assertDictEqual( + get_x_y_index_ranges_from_coordinates( + '/Soil_Moisture_Retrieval_Data_AM/surface_flag', + smap_varinfo, + smap_prefetch, + latitude_coordinate, + longitude_coordinate, + {}, + bounding_box=bbox, + shape_file_path=None, + ), + expected_index_ranges, + ) + + # Assertions don't like direct comparisons of numpy arrays, so + # have to extract the call arguments and compare those + mock_get_x_y_extents.assert_called_once_with( + ANY, ANY, crs, shape_file=None, bounding_box=bbox + ) + + actual_x_values = mock_get_x_y_extents.call_args_list[0][0][0] + actual_y_values = mock_get_x_y_extents.call_args_list[0][0][1] + + assert_array_equal(actual_x_values, smap_prefetch['projected_x'][:]) + assert_array_equal(actual_y_values, smap_prefetch['projected_y'][:]) + + self.assertEqual(mock_get_dimension_index_range.call_count, 2) + mock_get_dimension_index_range.assert_has_calls( + [ + call( + ANY, + x_y_extents['x_min'], + x_y_extents['x_max'], + bounds_values=None, + ), + call( + ANY, + x_y_extents['y_min'], + x_y_extents['y_max'], + bounds_values=None, + ), + ] + ) + assert_array_equal( + mock_get_dimension_index_range.call_args_list[0][0][0], + smap_prefetch['projected_x'][:], + ) + assert_array_equal( + mock_get_dimension_index_range.call_args_list[1][0][0], + smap_prefetch['projected_y'][:], + ) + + mock_get_x_y_extents.reset_mock() + mock_get_dimension_index_range.reset_mock() + + with self.subTest( + 'Non projected grid with no coordinates not try to get index ranges' + ): + with Dataset(smap_file_path, 'r') as smap_prefetch: + self.assertDictEqual( + get_x_y_index_ranges_from_coordinates( + 'projected_x', + smap_varinfo, + smap_prefetch, + latitude_coordinate, + longitude_coordinate, + {}, + bounding_box=bbox, + ), + {}, + ) + + mock_get_x_y_extents.assert_not_called() + mock_get_dimension_index_range.assert_not_called() + + with self.subTest('Function does not rederive known index ranges'): + with Dataset(smap_file_path, 'r') as smap_prefetch: + self.assertDictEqual( + get_projected_x_y_index_ranges( + 'Soil_Moisture_Retrieval_Data_AM/surface_flag', + smap_varinfo, + smap_prefetch, + expected_index_ranges, + bounding_box=bbox, + ), + {}, + ) + + mock_get_x_y_extents.assert_not_called() + mock_get_dimension_index_range.assert_not_called() + @patch('hoss.spatial.get_dimension_index_range') @patch('hoss.spatial.get_projected_x_y_extents') def test_get_projected_x_y_index_ranges( From 5c5eb855aefa92f606b5d73fb148a1b2d5008c95 Mon Sep 17 00:00:00 2001 From: sudhamurthy Date: Fri, 4 Oct 2024 14:51:48 -0400 Subject: [PATCH 32/41] DAS-2232 - removed accidental checkin of an incomplete unit test --- tests/unit/test_spatial.py | 148 ------------------------------------- 1 file changed, 148 deletions(-) diff --git a/tests/unit/test_spatial.py b/tests/unit/test_spatial.py index e0c72da..09df313 100644 --- a/tests/unit/test_spatial.py +++ b/tests/unit/test_spatial.py @@ -183,154 +183,6 @@ def test_get_spatial_index_ranges_geographic(self): {'/latitude': (5, 44), '/longitude': (160, 199)}, ) - @patch('hoss.spatial.get_dimension_index_range') - @patch('hoss.spatial.get_projected_x_y_extents') - def test_get_x_y_index_ranges_from_coordinates( - self, mock_get_x_y_extents, mock_get_dimension_index_range - ): - """Ensure that x and y index ranges are only requested only when there are - no projected dimensions and when there are coordinate datasets, - and the values have not already been calculated. - - The example used in this test is for the SMAP SPL3SMP collection, - (SMAP L3 Radiometer Global Daily 36 km EASE-Grid Soil Moisture) - which has a Equal-Area Scalable Earth Grid (EASE-Grid 2.0) CRS for - a projected grid which is lambert_cylindrical_equal_area projection - - """ - smap_varinfo = VarInfoFromDmr( - 'tests/data/SC_SPL3SMP_009.dmr', - 'SPL3SMP', - 'hoss/hoss_config.json', - ) - smap_file_path = 'tests/data/SC_SPL3SMP_009_prefetch.nc4' - expected_index_ranges = {'projected_x': (487, 595), 'projected_y': (9, 38)} - bbox = BBox(2, 54, 42, 72) - - latitude_coordinate = smap_varinfo.get_variable( - '/Soil_Moisture_Retrieval_Data_AM/latitude' - ) - longitude_coordinate = smap_varinfo.get_variable( - '/Soil_Moisture_Retrieval_Data_AM/longitude' - ) - - crs = CRS.from_cf( - { - 'false_easting': 0.0, - 'false_northing': 0.0, - 'longitude_of_central_meridian': 0.0, - 'standard_parallel': 30.0, - 'grid_mapping_name': 'lambert_cylindrical_equal_area', - } - ) - - x_y_extents = { - 'x_min': 192972.56050179302, - 'x_max': 4052423.7705376535, - 'y_min': 5930779.396449475, - 'y_max': 6979878.9118312765, - } - - mock_get_x_y_extents.return_value = x_y_extents - - # When ranges are derived, they are first calculated for x, then y: - mock_get_dimension_index_range.side_effect = [(487, 595), (9, 38)] - - with self.subTest( - 'Projected grid from coordinates gets expected dimension ranges' - ): - with Dataset(smap_file_path, 'r') as smap_prefetch: - self.assertDictEqual( - get_x_y_index_ranges_from_coordinates( - '/Soil_Moisture_Retrieval_Data_AM/surface_flag', - smap_varinfo, - smap_prefetch, - latitude_coordinate, - longitude_coordinate, - {}, - bounding_box=bbox, - shape_file_path=None, - ), - expected_index_ranges, - ) - - # Assertions don't like direct comparisons of numpy arrays, so - # have to extract the call arguments and compare those - mock_get_x_y_extents.assert_called_once_with( - ANY, ANY, crs, shape_file=None, bounding_box=bbox - ) - - actual_x_values = mock_get_x_y_extents.call_args_list[0][0][0] - actual_y_values = mock_get_x_y_extents.call_args_list[0][0][1] - - assert_array_equal(actual_x_values, smap_prefetch['projected_x'][:]) - assert_array_equal(actual_y_values, smap_prefetch['projected_y'][:]) - - self.assertEqual(mock_get_dimension_index_range.call_count, 2) - mock_get_dimension_index_range.assert_has_calls( - [ - call( - ANY, - x_y_extents['x_min'], - x_y_extents['x_max'], - bounds_values=None, - ), - call( - ANY, - x_y_extents['y_min'], - x_y_extents['y_max'], - bounds_values=None, - ), - ] - ) - assert_array_equal( - mock_get_dimension_index_range.call_args_list[0][0][0], - smap_prefetch['projected_x'][:], - ) - assert_array_equal( - mock_get_dimension_index_range.call_args_list[1][0][0], - smap_prefetch['projected_y'][:], - ) - - mock_get_x_y_extents.reset_mock() - mock_get_dimension_index_range.reset_mock() - - with self.subTest( - 'Non projected grid with no coordinates not try to get index ranges' - ): - with Dataset(smap_file_path, 'r') as smap_prefetch: - self.assertDictEqual( - get_x_y_index_ranges_from_coordinates( - 'projected_x', - smap_varinfo, - smap_prefetch, - latitude_coordinate, - longitude_coordinate, - {}, - bounding_box=bbox, - ), - {}, - ) - - mock_get_x_y_extents.assert_not_called() - mock_get_dimension_index_range.assert_not_called() - - with self.subTest('Function does not rederive known index ranges'): - with Dataset(smap_file_path, 'r') as smap_prefetch: - self.assertDictEqual( - get_projected_x_y_index_ranges( - 'Soil_Moisture_Retrieval_Data_AM/surface_flag', - smap_varinfo, - smap_prefetch, - expected_index_ranges, - bounding_box=bbox, - ), - {}, - ) - - mock_get_x_y_extents.assert_not_called() - mock_get_dimension_index_range.assert_not_called() - @patch('hoss.spatial.get_dimension_index_range') @patch('hoss.spatial.get_projected_x_y_extents') def test_get_projected_x_y_index_ranges( From 80c2fb2df2a9ca1a42221cc3da5bee42cd38d729 Mon Sep 17 00:00:00 2001 From: sudhamurthy Date: Mon, 7 Oct 2024 18:35:55 -0400 Subject: [PATCH 33/41] DAS-2232 - added unit test for the new method - get_x_y_index_ranges_from_coordinates --- hoss/spatial.py | 3 + tests/unit/test_spatial.py | 119 +++++++++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+) diff --git a/hoss/spatial.py b/hoss/spatial.py index 8cd5d8f..7b861f4 100644 --- a/hoss/spatial.py +++ b/hoss/spatial.py @@ -243,6 +243,7 @@ def get_x_y_index_ranges_from_coordinates( points. """ + crs = get_variable_crs(non_spatial_variable, varinfo) projected_x = 'projected_x' projected_y = 'projected_y' @@ -267,11 +268,13 @@ def get_x_y_index_ranges_from_coordinates( override_dimension_datasets[projected_x][:], x_y_extents['x_min'], x_y_extents['x_max'], + bounds_values=None, ) y_index_ranges = get_dimension_index_range( override_dimension_datasets[projected_y][:], x_y_extents['y_min'], x_y_extents['y_max'], + bounds_values=None, ) x_y_index_ranges = {projected_x: x_index_ranges, projected_y: y_index_ranges} else: diff --git a/tests/unit/test_spatial.py b/tests/unit/test_spatial.py index 09df313..b77f442 100644 --- a/tests/unit/test_spatial.py +++ b/tests/unit/test_spatial.py @@ -11,6 +11,7 @@ from varinfo import VarInfoFromDmr from hoss.bbox_utilities import BBox +from hoss.coordinate_utilities import update_dimension_variables from hoss.spatial import ( get_bounding_box_longitudes, get_geographic_index_range, @@ -183,6 +184,124 @@ def test_get_spatial_index_ranges_geographic(self): {'/latitude': (5, 44), '/longitude': (160, 199)}, ) + @patch('hoss.spatial.get_dimension_index_range') + @patch('hoss.spatial.get_projected_x_y_extents') + def test_get_x_y_index_ranges_from_coordinates( + self, + mock_get_x_y_extents, + mock_get_dimension_index_range, + ): + """Ensure that x and y index ranges are only requested only when there are + no projected dimensions and when there are coordinate datasets, + and the values have not already been calculated. + + The example used in this test is for the SMAP SPL3SMP collection, + (SMAP L3 Radiometer Global Daily 36 km EASE-Grid Soil Moisture) + which has a Equal-Area Scalable Earth Grid (EASE-Grid 2.0) CRS for + a projected grid which is lambert_cylindrical_equal_area projection + + """ + smap_varinfo = VarInfoFromDmr( + 'tests/data/SC_SPL3SMP_009.dmr', + 'SPL3SMP', + 'hoss/hoss_config.json', + ) + smap_file_path = 'tests/data/SC_SPL3SMP_009_prefetch.nc4' + expected_index_ranges = {'projected_x': (487, 595), 'projected_y': (9, 38)} + bbox = BBox(2, 54, 42, 72) + smap_variable_name = '/Soil_Moisture_Retrieval_Data_AM/surface_flag' + + latitude_coordinate = smap_varinfo.get_variable( + '/Soil_Moisture_Retrieval_Data_AM/latitude' + ) + longitude_coordinate = smap_varinfo.get_variable( + '/Soil_Moisture_Retrieval_Data_AM/longitude' + ) + + crs = CRS.from_cf( + { + 'false_easting': 0.0, + 'false_northing': 0.0, + 'longitude_of_central_meridian': 0.0, + 'standard_parallel': 30.0, + 'grid_mapping_name': 'lambert_cylindrical_equal_area', + } + ) + + x_y_extents = { + 'x_min': 192972.56050179302, + 'x_max': 4052423.7705376535, + 'y_min': 5930779.396449475, + 'y_max': 6979878.9118312765, + } + + mock_get_x_y_extents.return_value = x_y_extents + + # When ranges are derived, they are first calculated for x, then y: + mock_get_dimension_index_range.side_effect = [(487, 595), (9, 38)] + + with self.subTest( + 'Projected grid from coordinates gets expected dimension ranges' + ): + with Dataset(smap_file_path, 'r') as smap_prefetch: + self.assertDictEqual( + get_x_y_index_ranges_from_coordinates( + '/Soil_Moisture_Retrieval_Data_AM/surface_flag', + smap_varinfo, + smap_prefetch, + latitude_coordinate, + longitude_coordinate, + {}, + bounding_box=bbox, + shape_file_path=None, + ), + expected_index_ranges, + ) + + mock_get_x_y_extents.assert_called_once_with( + ANY, ANY, crs, shape_file=None, bounding_box=bbox + ) + + self.assertEqual(mock_get_dimension_index_range.call_count, 2) + mock_get_dimension_index_range.assert_has_calls( + [ + call( + ANY, + x_y_extents['x_min'], + x_y_extents['x_max'], + bounds_values=None, + ), + call( + ANY, + x_y_extents['y_min'], + x_y_extents['y_max'], + bounds_values=None, + ), + ] + ) + + mock_get_x_y_extents.reset_mock() + mock_get_dimension_index_range.reset_mock() + + with self.subTest('Function does not rederive known index ranges'): + with Dataset(smap_file_path, 'r') as smap_prefetch: + self.assertDictEqual( + get_x_y_index_ranges_from_coordinates( + '/Soil_Moisture_Retrieval_Data_AM/surface_flag', + smap_varinfo, + smap_prefetch, + latitude_coordinate, + longitude_coordinate, + expected_index_ranges, + bounding_box=bbox, + shape_file_path=None, + ), + {}, + ) + + mock_get_x_y_extents.assert_not_called() + mock_get_dimension_index_range.assert_not_called() + @patch('hoss.spatial.get_dimension_index_range') @patch('hoss.spatial.get_projected_x_y_extents') def test_get_projected_x_y_index_ranges( From 30eccd00f20985744295a6f8b3d18e40e251fd68 Mon Sep 17 00:00:00 2001 From: sudhamurthy Date: Tue, 8 Oct 2024 01:25:10 -0400 Subject: [PATCH 34/41] DAS-2232 - added unit test for the new method - get_x_y_index_ranges_from_coordinates --- tests/data/SC_SPL3SMP_008.dmr | 2778 ++++++++++++++++++++++++ tests/data/SC_SPL3SMP_008_prefetch.nc4 | Bin 0 -> 337938 bytes tests/unit/test_spatial.py | 5 +- 3 files changed, 2780 insertions(+), 3 deletions(-) create mode 100644 tests/data/SC_SPL3SMP_008.dmr create mode 100644 tests/data/SC_SPL3SMP_008_prefetch.nc4 diff --git a/tests/data/SC_SPL3SMP_008.dmr b/tests/data/SC_SPL3SMP_008.dmr new file mode 100644 index 0000000..b45abae --- /dev/null +++ b/tests/data/SC_SPL3SMP_008.dmr @@ -0,0 +1,2778 @@ + + + + 3.21.0-428 + + + 3.21.0-428 + + + libdap-3.21.0-103 + + + +# TheBESKeys::get_as_config() +AllowedHosts=^https?:\/\/ +BES.Catalog.catalog.FollowSymLinks=Yes +BES.Catalog.catalog.RootDirectory=/usr/share/hyrax +BES.Catalog.catalog.TypeMatch=dmrpp:.*\.(dmrpp)$; +BES.Catalog.catalog.TypeMatch+=h5:.*(\.bz2|\.gz|\.Z)?$; +BES.Data.RootDirectory=/dev/null +BES.LogName=./bes.log +BES.UncompressCache.dir=/tmp/hyrax_ux +BES.UncompressCache.prefix=ux_ +BES.UncompressCache.size=500 +BES.module.cmd=/usr/lib64/bes/libdap_xml_module.so +BES.module.dap=/usr/lib64/bes/libdap_module.so +BES.module.dmrpp=/usr/lib64/bes/libdmrpp_module.so +BES.module.h5=/usr/lib64/bes/libhdf5_module.so +BES.modules=dap,cmd,h5,dmrpp +H5.DefaultHandleDimensions=true +H5.EnableCF=false +H5.EnableCheckNameClashing=true + + + + build_dmrpp -c /tmp/bes_conf_IAud -f /usr/share/hyrax/DATA/SMAP_L3_SM_P_20150331_R18290_002.h5 -r /tmp/dmr__0PmwFd -u OPeNDAP_DMRpp_DATA_ACCESS_URL -M + + + + + + + The SMAP observatory houses an L-band radiometer that operates at 1.414 GHz and an L-band radar that operates at 1.225 GHz. The instruments share a rotating reflector antenna with a 6 meter aperture that scans over a 1000 km swath. The bus is a 3 axis stabilized spacecraft that provides momentum compensation for the rotating antenna. + + + 14.60000038 + + + SMAP + + + + + JPL CL#14-2285, JPL 400-1567 + + + SMAP Handbook + + + 2014-07-01 + + + + + JPL CL#14-2285, JPL 400-1567 + + + SMAP Handbook + + + 2014-07-01 + + + + + The SMAP 1.414 GHz L-Band Radiometer + + + L-Band Radiometer + + + SMAP RAD + + + + + The SMAP 1.225 GHz L-Band Radar Instrument + + + L-Band Synthetic Aperture Radar + + + SMAP SAR + + + + + JPL CL#14-2285, JPL 400-1567 + + + SMAP Handbook + + + 2014-07-01 + + + + + + soil_moisture + + + + Percentage of EASE2 grid cells with Retrieved Soil Moistures outside the Acceptable Range. + + + Percentage of EASE2 grid cells with soil moisture measures that fall outside of a predefined acceptable range. + + + directInternal + + + percent + + + 100. + + + + + Percentage of EASE2 grid cells that lack soil moisture retrieval values relative to the total number of grid cells where soil moisture retrieval was attempted. + + + Percent of Missing Data + + + percent + + + 176.5011139 + + + directInternal + + + + + + eng + + + utf8 + + + 1.0 + + + Product Specification Document for the SMAP Level 3 Passive Soil Moisture Product (L3_SM_P) + + + 2013-02-08 + + + L3_SM_P + + + + + doi:10.5067/OMHVSRGFX38O + + + SPL3SMP + + + SMAP + + + utf8 + + + National Aeronautics and Space Administration (NASA) + + + eng + + + 008 + + + Daily global composite of up-to 30 half-orbit L2_SM_P soil moisture estimates based on radiometer brightness temperature measurements acquired by the SMAP radiometer during ascending and descending half-orbits at approximately 6 PM and 6 AM local solar time. + + + onGoing + + + 2021-08-31 + + + The software that generates the Level 3 Soil Moisture Passive product and the data system that automates its production were designed and implemented + at the Jet Propulsion Laboratory, California Institute of Technology in Pasadena, California. + + + geoscientificInformation + + + The SMAP L3_SM_P algorithm provides daily global composite of soil moistures based on radiometer data on a 36 km grid. + + + SMAP L3 Radiometer Global Daily 36 km EASE-Grid Soil Moisture + + + National Snow and Ice Data Center + + + R18 + + + grid + + + The Calibration and Validation Version 2 Release of the SMAP Level 3 Daily Global Composite Passive Soil Moisture Science Processing Software. + + + + + SPL3SMP + + + utf8 + + + be9694f7-6503-4c42-8423-4c824df6c1f2 + + + eng + + + 1.8.13 + + + 008 + + + Daily global composite of up-to 15 half-orbit L2_SM_P soil moisture estimates based on radiometer brightness temperature measurements acquired by the SMAP radiometer during descending half-orbits at approximately 6 AM local solar time. + + + 2022-03-11 + + + onGoing + + + The software that generates the Level 3 SM_P product and the data system that automates its production were designed and implemented at the Jet Propulsion Laboratory, California Institute of Technology in Pasadena, California. + + + geoscientificInformation + + + The SMAP L3_SM_P effort provides soil moistures based on radiometer data on a 36 km grid. + + + HDF5 + + + SMAP_L3_SM_P_20150331_R18290_002.h5 + + + asNeeded + + + R18290 + + + Jet Propulsion Laboratory + + + 2016-05-01 + + + L3_SM_P + + + grid + + + The Calibration and Validation Version 2 Release of the SMAP Level 3 Daily Global Composite Passive Soil Moisture Science Processing Software. + + + + + Soil moisture is retrieved over land targets on the descending (AM) SMAP half-orbits when the SMAP spacecraft is travelling from North to South, while the SMAP instruments are operating in the nominal mode. The L3_SM_P product represents soil moisture retrieved over the entre UTC day. Retrievals are performed but flagged as questionable over urban areas, mountainous areas with high elevation variability, and areas with high ( &gt; 5 kg/m**2) vegetation water content; for retrievals using the high-resolution radar, cells in the nadir region are also flagged. Retrievals are inhibited for permanent snow/ice, frozen ground, and excessive static or transient open water in the cell, and for excessive RFI in the sensor data. + + + 180. + + + -180. + + + -85.04450226 + + + 85.04450226 + + + 2015-03-31T00:00:00.000Z + + + 2015-03-31T23:59:59.999Z + + + + + SMAP_L3_SM_P_20150331_R18290_002.qa + + + An ASCII product that contains statistical information on data product results. These statistics enable data producers and users to assess the quality of the data in the data product granule. + + + 2022-03-11 + + + + + 0 + + + 0 + + + 0 + + + 2 + + + SMAP Fixed Earth Grids, SMAP Science Document no: 033, May 11, 2009 + + + point + + + + 406 + + + 36. + + + + + EASE-Grid 2.0 + + + EASE-Grid 2.0: Incremental but Significant Improvements for Earth-Gridded Data Sets (ISPRS Int. J. Geo-Inf. 2012, 1, 32-45; doi:10.3390/ijgi1010032) + + + 2012-03-31 + + + + + 964 + + + 36. + + + + + The Equal-Area Scalable Earth Grid (EASE-Grid 2.0) is used for gridding satellite data sets. The EASE-Grid 2.0 is defined on the WGS84 ellipsoid to allow users to import data into standard GIS software formats such as GeoTIFF without reprojection. + + + Equal-Area Scalable Earth Grid + + + + + + 869 + + + 864 + + + + + + A configuration file that specifies the complete set of elements within the input Level 2 SM_P Product that the Radiometer Level 3_SM_P Science Processing Software (SPS) needs in order to function. + + + R18290 + + + SMAP_L3_SM_P_SPS_InputConfig_L2_SM_P.xml + + + 2022-03-11 + + + + + Precomputed longitude at each EASEGrid cell on 36-km grid on Global Cylindrical Projection + + + 002 + + + EZ2Lon_M36_002.float32 + + + 2013-05-09 + + + + + Passive soil moisture estimates onto a 36-km global Earth-fixed grid, based on radiometer measurements acquired when the SMAP spacecraft is travelling from North to South at approximately 6:00 AM local time. + + + SMAP_L2_SM_P_00864_D_20150331T162851_R18290_001.h5 + SMAP_L2_SM_P_00865_A_20150331T162945_R18290_001.h5 + SMAP_L2_SM_P_00865_D_20150331T171859_R18290_001.h5 + SMAP_L2_SM_P_00866_A_20150331T180814_R18290_001.h5 + SMAP_L2_SM_P_00866_D_20150331T185725_R18290_001.h5 + SMAP_L2_SM_P_00867_A_20150331T194640_R18290_001.h5 + SMAP_L2_SM_P_00867_D_20150331T203555_R18290_001.h5 + SMAP_L2_SM_P_00868_A_20150331T212510_R18290_001.h5 + SMAP_L2_SM_P_00868_D_20150331T221420_R18290_001.h5 + SMAP_L2_SM_P_00869_A_20150331T230335_R18290_001.h5 + SMAP_L2_SM_P_00869_D_20150331T235250_R18290_001.h5 + + + doi:10.5067/LPJ8F0TAK6E0 + + + L2_SM_P + + + 2022-02-22 + 2022-02-22 + 2022-02-22 + 2022-02-22 + 2022-02-22 + 2022-02-22 + 2022-02-22 + 2022-02-22 + 2022-02-22 + 2022-02-22 + 2022-02-22 + + + 36. + + + + + A configuration file that specifies the source of the values for each of the data elements that comprise the metadata in the output Radiometer Level 3_SM_P product. + + + R18290 + + + SMAP_L3_SM_P_SPS_MetConfig_L2_SM_P.xml + + + 2022-03-11 + + + + + A configuration file that lists the entire content of the output Radiometer Level 3_SM_P_ product. + + + R18290 + + + SMAP_L3_SM_P_SPS_OutputConfig_L3_SM_P.xml + + + 2022-03-11 + + + + + A configuration file generated automatically within the SMAP data system that specifies all of the conditions required for each individual run of the Radiometer Level 3 SM P Science Processing Software (SPS). + + + R18290 + + + SMAP_L3_SM_P_SPS_RunConfig_20220311T185327319.xml + + + 2022-03-11 + + + + + + Soil moisture retrieved using default retrieval algorithm from brightness temperatures acquired by the SMAP radiometer during the spacecraft descending pass. Level 2 granule data are then mosaicked on a daily basis to form the Level 3 product. + + + 2022-03-11T18:53:27.319Z + + + Algorithm Theoretical Basis Document: SMAP L2 and L3 Radiometer Soil Moisture (Passive) Data Products: L2_SM_P &amp; L3_SM_P + + + 2000-01-01T11:58:55.816Z + + + 023 + + + L3_SM_P_SPS + + + 2021-08-17 + + + 015 + + + L3_SM_P_SPS + + + Version 1.1 + + + 2019-04-15 + + + 2015-10-30 + + + 026 + + + Level 3 soil moisture product is formed by mosaicking Level 2 soil moisture granule data acquired over one day. + + + Algorithm Theoretical Basis Document: SMAP L2 and L3 Radiometer Soil Moisture (Passive) Data Products: L2_SM_P &amp; L3_SM_P + + + 2451545. + + + J2000 + + + 2012-10-26 + + + Soil Moisture Active Passive Mission (SMAP) Science Data System (SDS) Operations Facility + + + Soil Moisture Active Passive (SMAP) Radiometer processing algorithm + + + Preliminary + + + + + + + + + Longitude of the center of the Earth based grid cell. + + + degrees_east + + + + + + + Latitude of the center of the Earth based grid cell. + + + degrees_north + + + + + + + 0. + + + The fraction of the area of the 36 km grid cell that is covered by static water based on a Digital Elevation Map. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 1. + + + -9999. + + + + + + + + + Representative SCA-V soil moisture measurement for the Earth based grid cell. + + + cm**3/cm**3 + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0.01999999955 + + + 0.5 + + + -9999. + + + + + + + + + Representative angle between the antenna boresight vector and the normal to the Earth&apos;s surface for all footprints within the cell. + + + degrees + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 90. + + + -9999. + + + + + + + + + Arithmetic average of the acquisition time of all of the brightness temperature footprints with a center that falls within the EASE grid cell in UTC. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + + + + + + + 65534 + + + 1s, 2s, 4s, 8s + + + Bit flags that record the conditions and the quality of the DCA retrieval algorithms that generate soil moisture for the grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + Retrieval_recommended Retrieval_attempted Retrieval_success FT_retrieval_success + + + + + + + + + The measured opacity of the vegetation used in the DCA retrieval in the grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + -999999.875 + + + 999999.875 + + + -9999. + + + + + + + + + Bit flags that represent the quality of the horizontal polarization brightness temperature within each grid cell + + + 65534 + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 1s, 2s, 4s, 8s, 16s, 32s, 64s, 128s, 256s, 512s, 1024s, 2048s, 4096s, 8192s, 16384s, 32768s + + + Horizontal_polarization_quality Horizontal_polarization_range Horizontal_polarization_RFI_detection Horizontal_polarization_RFI_correction Horizontal_polarization_NEDT Horizontal_polarization_direct_sun_correction Horizontal_polarization_reflected_sun_correction Horizontal_polarization_reflected_moon_correction Horizontal_polarization_direct_galaxy_correction Horizontal_polarization_reflected_galaxy_correction Horizontal_polarization_atmosphere_correction Horizontal_polarization_Faraday_rotation_correction Horizontal_polarization_null_value_bit Horizontal_polarization_water_correction Horizontal_polarization_RFI_check Horizontal_polarization_RFI_clean + + + + + + + + + A unitless value that is indicative of bare soil roughness used in DCA within the 36 km grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 1. + + + -9999. + + + + + + + + + + 254 + + + An enumerated type that specifies the most common landcover class in the grid cell based on the IGBP landcover map. The array order is longitude (ascending), followed by latitude (descending), and followed by IGBP land cover type descending dominance (only the first three types are listed) + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + + + + + + + The row index of the 36 km EASE grid cell that contains the associated data. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0 + + + 405 + + + 65534 + + + + + + + + + Horizontal polarization brightness temperature in 36 km Earth grid cell before adjustment for the presence of water bodies. + + + Kelvin + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 330. + + + -9999. + + + + + + + + + Vertical polarization brightness temperature in 36 km Earth grid cell adjusted for the presence of water bodies. + + + Kelvin + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 330. + + + -9999. + + + + + + + + + Fourth stokes parameter for each 36 km grid cell calculated with an adjustment for the presence of water bodies + + + Kelvin + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 330. + + + -9999. + + + + + + + + + Weighted average of the longitude of the center of the brightness temperature footprints that fall within the EASE grid cell. + + + degrees + + + -180. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 179.9989929 + + + -9999. + + + + + + + + + The measured opacity of the vegetation used in the SCA-H retrieval in the grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + -999999.875 + + + 999999.875 + + + -9999. + + + + + + + + + 65534 + + + 1s, 2s, 4s, 8s + + + Bit flags that record the conditions and the quality of the SCA-H retrieval algorithms that generate soil moisture for the grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + Retrieval_recommended Retrieval_attempted Retrieval_success FT_retrieval_success + + + + + + + + + A unitless value that is indicative of bare soil roughness used in DCA within the 36 km grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 1. + + + -9999. + + + /Soil_Moisture_Retrieval_Data_AM/roughness_coefficient + + + + + + + + + Diffuse reflecting power of the Earth&apos;s surface used in SCA-H within the grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 1. + + + -9999. + + + + + + + + + + 0. + + + The fraction of the grid cell that contains the most common land cover in that area based on the IGBP landcover map. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 1. + + + -9999. + + + + + + + + + 0. + + + Diffuse reflecting power of the Earth&apos;s surface used in SCA-V within the grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 1. + + + -9999. + + + + + + + + + 65534 + + + 1s, 2s, 4s, 8s + + + Bit flags that record the conditions and the quality of the DCA retrieval algorithms that generate soil moisture for the grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + Retrieval_recommended Retrieval_attempted Retrieval_success FT_retrieval_success + + + /Soil_Moisture_Retrieval_Data_AM/retrieval_qual_flag_dca + + + + + + + + + Representative measure of water in the vegetation within the 36 km grid cell. + + + kg/m**2 + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 20. + + + -9999. + + + + + + + + + Vertical polarization brightness temperature in 36 km Earth grid cell before adjustment for the presence of water bodies. + + + Kelvin + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 330. + + + -9999. + + + + + + + + + A unitless value that is indicative of bare soil roughness used in SCA-V within the 36 km grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 1. + + + -9999. + + + + + + + + + Representative SCA-H soil moisture measurement for the Earth based grid cell. + + + cm**3/cm**3 + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0.01999999955 + + + 0.5 + + + -9999. + + + + + + + + + Gain weighted fraction of static water within the radiometer horizontal polarization brightness temperature antenna pattern in 36 km Earth grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 1. + + + -9999. + + + + + + + + + A unitless value that is indicative of bare soil roughness used in SCA-H within the 36 km grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 1. + + + -9999. + + + + + + + + + Representative DCA soil moisture measurement for the Earth based grid cell. + + + cm**3/cm**3 + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0.01999999955 + + + 0.5 + + + -9999. + + + + + + + + + Third stokes parameter for each 36 km grid cell calculated with an adjustment for the presence of water bodies + + + Kelvin + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 330. + + + -9999. + + + + + + + + + 65534 + + + Bit flags that represent the quality of the 3rd Stokes brightness temperature within each grid cell + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 1s, 2s, 4s, 8s, 16s, 32s, 64s, 128s, 256s, 512s, 1024s, 4096s, 16384s, 32768s + + + 3rd_Stokes_quality 3rd_Stokes_range 3rd_Stokes_RFI_detection 3rd_Stokes_RFI_correction 3rd_Stokes_NEDT 3rd_Stokes_direct_sun_correction 3rd_Stokes_reflected_sun_correction 3rd_Stokes_reflected_moon_correction 3rd_Stokes_direct_galaxy_correction 3rd_Stokes_reflected_galaxy_correction 3rd_Stokes_atmosphere_correction 3rd_Stokes_null_value_bit 3rd_Stokes_RFI_check 3rd_Stokes_RFI_clean + + + + + + + + + 65534 + + + Bit flags that represent the quality of the 4th Stokes brightness temperature within each grid cell + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 1s, 2s, 4s, 8s, 16s, 32s, 64s, 128s, 256s, 512s, 1024s, 4096s, 16384s, 32768s + + + 4th_Stokes_quality 4th_Stokes_range 4th_Stokes_RFI_detection 4th_Stokes_RFI_correction 4th_Stokes_NEDT 4th_Stokes_direct_sun_correction 4th_Stokes_reflected_sun_correction 4th_Stokes_reflected_moon_correction 4th_Stokes_direct_galaxy_correction 4th_Stokes_reflected_galaxy_correction 4th_Stokes_atmosphere_correction 4th_Stokes_null_value_bit 4th_Stokes_RFI_check 4th_Stokes_RFI_clean + + + + + + + + + Horizontal polarization brightness temperature in 36 km Earth grid cell adjusted for the presence of water bodies. + + + Kelvin + + + 0. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 330. + + + -9999. + + + + + + + + + Gain weighted fraction of static water within the radiometer vertical polarization brightness temperature antenna pattern in 36 km Earth grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 1. + + + -9999. + + + + + + + + + Diffuse reflecting power of the Earth&apos;s surface used in DCA within the grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 1. + + + -9999. + + + + + + + + + Indicates if the grid point lies on land (0) or water (1). + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0 + + + 1 + + + 65534 + + + + + + + + + Arithmetic average of the acquisition time of all of the brightness temperature footprints with a center that falls within the EASE grid cell in seconds since noon on January 1, 2000 UTC. + + + seconds + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + -999999.90000000002 + + + 940000000. + + + -9999. + + + + + + + + + The measured opacity of the vegetation used in the DCA retrieval in the grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + -999999.875 + + + 999999.875 + + + -9999. + + + /Soil_Moisture_Retrieval_Data_AM/vegetation_opacity + + + + + + + + + A unitless value that is indicative of aggregated bulk_density within the 36 km grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 2.650000095 + + + -9999. + + + + + + + + + Net uncertainty measure of soil moisture measure for the Earth based grid cell. - Calculation method is TBD. + + + cm**3/cm**3 + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 0.200000003 + + + -9999. + + + + + + + + + The fraction of the area of the 36 km grid cell that is covered by water based on the radar detection algorithm. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 1. + + + -9999. + + + + + + + + + 65534 + + + Bit flags that represent the quality of the vertical polarization brightness temperature within each grid cell + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 1s, 2s, 4s, 8s, 16s, 32s, 64s, 128s, 256s, 512s, 1024s, 2048s, 4096s, 8192s, 16384s, 32768s + + + Vertical_polarization_quality Vertical_polarization_range Vertical_polarization_RFI_detection Vertical_polarization_RFI_correction Vertical_polarization_NEDT Vertical_polarization_direct_sun_correction Vertical_polarization_reflected_sun_correction Vertical_polarization_reflected_moon_correction Vertical_polarization_direct_galaxy_correction Vertical_polarization_reflected_galaxy_correction Vertical_polarization_atmosphere_correction Vertical_polarization_Faraday_rotation_correction Vertical_polarization_null_value_bit Vertical_polarization_water_correction Vertical_polarization_RFI_check Vertical_polarization_RFI_clean + + + + + + + + + Weighted average of the latitude of the center of the brightness temperature footprints that fall within the EASE grid cell. + + + degrees + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + -90. + + + 90. + + + -9999. + + + + + + + + + The column index of the 36 km EASE grid cell that contains the associated data. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0 + + + 963 + + + 65534 + + + + + + + + + Temperature at land surface based on GMAO GEOS-5 data. + + + Kelvins + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 350. + + + -9999. + + + + + + + + + Representative DCA soil moisture measurement for the Earth based grid cell. + + + cm**3/cm**3 + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0.01999999955 + + + 0.5 + + + -9999. + + + /Soil_Moisture_Retrieval_Data_AM/soil_moisture + + + + + + + + + 65534 + + + 1s, 2s, 4s, 8s + + + Bit flags that record the conditions and the quality of the SCA-V retrieval algorithms that generate soil moisture for the grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + Retrieval_recommended Retrieval_attempted Retrieval_success FT_retrieval_success + + + + + + + + + Fraction of the 36 km grid cell that is denoted as frozen. Based on binary flag that specifies freeze thaw conditions in each of the component 3 km grid cells. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 1. + + + -9999. + + + + + + + + + Bit flags that record ambient surface conditions for the grid cell + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 65534 + + + 1s, 2s, 4s, 8s, 16s, 32s, 64s, 128s, 256s, 512s, 1024s, 2048s + + + 36_km_static_water_body 36_km_radar_water_body_detection 36_km_coastal_proximity 36_km_urban_area 36_km_precipitation 36_km_snow_or_ice 36_km_permanent_snow_or_ice 36_km_radiometer_frozen_ground 36_km_model_frozen_ground 36_km_mountainous_terrain 36_km_dense_vegetation 36_km_nadir_region + + + + + + + + + The measured opacity of the vegetation used in the SCA-V retrieval in the grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + -999999.875 + + + 999999.875 + + + -9999. + + + + + + + + + Diffuse reflecting power of the Earth&apos;s surface used in DCA within the grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 1. + + + -9999. + + + /Soil_Moisture_Retrieval_Data_AM/albedo + + + + + + + + + A unitless value that is indicative of aggregated clay fraction within the 36 km grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 1. + + + -9999. + + + + + + + + + + + A unitless value that is indicative of aggregated bulk density within the 36 km grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 2.650000095 + + + -9999. + + + + + + + Representative angle between the antenna boresight vector and the normal to the Earth&apos;s surface for all footprints within the cell. + + + degrees + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 90. + + + -9999. + + + + + + + The row index of the 36 km EASE grid cell that contains the associated data. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0 + + + 405 + + + 65534 + + + + + + + The fraction of the area of the 36 km grid cell that is covered by static water based on a Digital Elevation Map. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 1. + + + -9999. + + + + + + + Fraction of the 36 km grid cell that is denoted as frozen. Based on binary flag that specifies freeze thaw conditions in each of the component 3 km grid cells. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 1. + + + -9999. + + + + + + + A unitless value that is indicative of bare soil roughness used in DCA retrievals within the 36 km grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 1. + + + -9999. + + + + + + + A unitless value that is indicative of bare soil roughness used in DCA retrievals within the 36 km grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 1. + + + -9999. + + + /Soil_Moisture_Retrieval_Data_PM/roughness_coefficient_dca_pm + + + + + + + Bit flags that record ambient surface conditions for the grid cell + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 65534 + + + 1s, 2s, 4s, 8s, 16s, 32s, 64s, 128s, 256s, 512s, 1024s, 2048s + + + 36_km_static_water_body 36_km_radar_water_body_detection 36_km_coastal_proximity 36_km_urban_area 36_km_precipitation 36_km_snow_or_ice 36_km_permanent_snow_or_ice 36_km_radar_frozen_ground 36_km_model_frozen_ground 36_km_mountainous_terrain 36_km_dense_vegetation 36_km_nadir_region + + + + + + + 65534 + + + 1s, 2s, 4s, 8s + + + Bit flags that record the conditions and the quality of the DCA retrieval algorithms that generate soil moisture for the grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + Retrieval_recommended Retrieval_attempted Retrieval_success FT_retrieval_success + + + + + + + Horizontal polarization brightness temperature in 36 km Earth grid cell adjusted for the presence of water bodies. + + + Kelvin + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 330. + + + -9999. + + + + + + + 65534 + + + 1s, 2s, 4s, 8s + + + Bit flags that record the conditions and the quality of the SCA-V retrieval algorithms that generate soil moisture for the grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + Retrieval_recommended Retrieval_attempted Retrieval_success FT_retrieval_success + + + + + + + Representative DCA soil moisture measurement for the Earth based grid cell. + + + cm**3/cm**3 + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0.01999999955 + + + 0.5 + + + -9999. + + + + + + + Diffuse reflecting power of the Earth&apos;s surface used in SCA-V retrievals within the grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 1. + + + -9999. + + + + + + + 0. + + + Gain weighted fraction of static water within the radiometer vertical polarization brightness temperature antenna pattern in 36 km Earth grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 1. + + + -9999. + + + + + + + A unitless value that is indicative of bare soil roughness used in SCA-V retrievals within the 36 km grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 1. + + + -9999. + + + + + + + 65534 + + + Bit flags that represent the quality of the 4th Stokes brightness temperature within each grid cell + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 1s, 2s, 4s, 8s, 16s, 32s, 64s, 128s, 256s, 512s, 1024s, 4096s, 16384s, 32768s + + + 4th_Stokes_quality 4th_Stokes_range 4th_Stokes_RFI_detection 4th_Stokes_RFI_correction 4th_Stokes_NEDT 4th_Stokes_direct_sun_correction 4th_Stokes_reflected_sun_correction 4th_Stokes_reflected_moon_correction 4th_Stokes_direct_galaxy_correction 4th_Stokes_reflected_galaxy_correction 4th_Stokes_atmosphere_correction 4th_Stokes_null_value_bit 4th_Stokes_RFI_check 4th_Stokes_RFI_clean + + + + + + + Third stokes parameter for each 36 km grid cell calculated with an adjustment for the presence of water bodies + + + Kelvin + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 330. + + + -9999. + + + + + + + 65534 + + + 1s, 2s, 4s, 8s + + + Bit flags that record the conditions and the quality of the DCA retrieval algorithms that generate soil moisture for the grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + Retrieval_recommended Retrieval_attempted Retrieval_success FT_retrieval_success + + + /Soil_Moisture_Retrieval_Data_PM/retrieval_qual_flag_pm + + + + + + + Representative SCA-V soil moisture measurement for the Earth based grid cell. + + + cm**3/cm**3 + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0.01999999955 + + + 0.5 + + + -9999. + + + + + + + 65534 + + + 1s, 2s, 4s, 8s + + + Bit flags that record the conditions and the quality of the SCA-H retrieval algorithms that generate soil moisture for the grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + Retrieval_recommended Retrieval_attempted Retrieval_success FT_retrieval_success + + + + + + + The measured opacity of the vegetation used in SCA-H retrievals in the grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + -999999.875 + + + 999999.875 + + + -9999. + + + + + + + Kelvin + + + 0. + + + Vertical polarization brightness temperature in 36 km Earth grid cell before adjustment for the presence of water bodies. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 330. + + + -9999. + + + + + + + A unitless value that is indicative of bare soil roughness used in SCA-H retrievals within the 36 km grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 1. + + + -9999. + + + + + + + Bit flags that represent the quality of the horizontal polarization brightness temperature within each grid cell + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 65534 + + + 1s, 2s, 4s, 8s, 16s, 32s, 64s, 128s, 256s, 512s, 1024s, 2048s, 4096s, 8192s, 16384s, 32768s + + + Horizontal_polarization_quality Horizontal_polarization_range Horizontal_polarization_RFI_detection Horizontal_polarization_RFI_correction Horizontal_polarization_NEDT Horizontal_polarization_direct_sun_correction Horizontal_polarization_reflected_sun_correction Horizontal_polarization_reflected_moon_correction Horizontal_polarization_direct_galaxy_correction Horizontal_polarization_reflected_galaxy_correction Horizontal_polarization_atmosphere_correction Horizontal_polarization_Faraday_rotation_correction Horizontal_polarization_null_value_bit Horizontal_polarization_water_correction Horizontal_polarization_RFI_check Horizontal_polarization_RFI_clean + + + + + + + A unitless value that is indicative of aggregated clay fraction within the 36 km grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 1. + + + -9999. + + + + + + + Weighted average of the latitude of the center of the brightness temperature footprints that fall within the EASE grid cell. + + + degrees + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + -90. + + + 90. + + + -9999. + + + + + + + The column index of the 36 km EASE grid cell that contains the associated data. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0 + + + 963 + + + 65534 + + + + + + + Arithmetic average of the acquisition time of all of the brightness temperature footprints with a center that falls within the EASE grid cell in seconds since noon on January 1, 2000 UTC. + + + seconds + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + -999999.90000000002 + + + 940000000. + + + -9999. + + + + + + + Fourth stokes parameter for each 36 km grid cell calculated with an adjustment for the presence of water bodies + + + Kelvin + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 330. + + + -9999. + + + + + + + Net uncertainty measure of soil moisture measure for the Earth based grid cell. - Calculation method is TBD. + + + cm**3/cm**3 + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 0.200000003 + + + -9999. + + + + + + + + 254 + + + An enumerated type that specifies the most common landcover class in the grid cell based on the IGBP landcover map. The array order is longitude (ascending), followed by latitude (descending), and followed by IGBP land cover type descending dominance (only the first three types are listed) + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + + + + + Arithmetic average of the acquisition time of all of the brightness temperature footprints with a center that falls within the EASE grid cell in UTC. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + + + + + Diffuse reflecting power of the Earth&apos;s surface used in DCA retrievals within the grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 1. + + + -9999. + + + + + + + Longitude of the center of the Earth based grid cell. + + + degrees_east + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + -180. + + + 179.9989929 + + + -9999. + + + + + + + 65534 + + + Bit flags that represent the quality of the 3rd Stokes brightness temperature within each grid cell + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 1s, 2s, 4s, 8s, 16s, 32s, 64s, 128s, 256s, 512s, 1024s, 4096s, 16384s, 32768s + + + 3rd_Stokes_quality 3rd_Stokes_range 3rd_Stokes_RFI_detection 3rd_Stokes_RFI_correction 3rd_Stokes_NEDT 3rd_Stokes_direct_sun_correction 3rd_Stokes_reflected_sun_correction 3rd_Stokes_reflected_moon_correction 3rd_Stokes_direct_galaxy_correction 3rd_Stokes_reflected_galaxy_correction 3rd_Stokes_atmosphere_correction 3rd_Stokes_null_value_bit 3rd_Stokes_RFI_check 3rd_Stokes_RFI_clean + + + + + + + The measured opacity of the vegetation used in DCA retrievals in the grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + -999999.875 + + + 999999.875 + + + -9999. + + + + + + + Bit flags that represent the quality of the vertical polarization brightness temperature within each grid cell + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 65534 + + + 1s, 2s, 4s, 8s, 16s, 32s, 64s, 128s, 256s, 512s, 1024s, 2048s, 4096s, 8192s, 16384s, 32768s + + + Vertical_polarization_quality Vertical_polarization_range Vertical_polarization_RFI_detection Vertical_polarization_RFI_correction Vertical_polarization_NEDT Vertical_polarization_direct_sun_correction Vertical_polarization_reflected_sun_correction Vertical_polarization_reflected_moon_correction Vertical_polarization_direct_galaxy_correction Vertical_polarization_reflected_galaxy_correction Vertical_polarization_atmosphere_correction Vertical_polarization_Faraday_rotation_correction Vertical_polarization_null_value_bit Vertical_polarization_water_correction Vertical_polarization_RFI_check Vertical_polarization_RFI_clean + + + + + + + 0. + + + The fraction of the area of the 36 km grid cell that is covered by water based on the radar detection algorithm. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 1. + + + -9999. + + + + + + + Representative SCA-H soil moisture measurement for the Earth based grid cell. + + + cm**3/cm**3 + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0.01999999955 + + + 0.5 + + + -9999. + + + + + + + Representative DCA soil moisture measurement for the Earth based grid cell. + + + cm**3/cm**3 + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0.01999999955 + + + 0.5 + + + -9999. + + + /Soil_Moisture_Retrieval_Data_PM/soil_moisture_pm + + + + + + + + 0. + + + The fraction of the grid cell that contains the most common land cover in that area based on the IGBP landcover map. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 1. + + + -9999. + + + + + + + The measured opacity of the vegetation used in DCA retrievals in the grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + -999999.875 + + + 999999.875 + + + -9999. + + + /Soil_Moisture_Retrieval_Data_PM/vegetation_opacity_dca_pm + + + + + + + 0. + + + Gain weighted fraction of static water within the radiometer horizontal polarization brightness temperature antenna pattern in 36 km Earth grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 1. + + + -9999. + + + + + + + Weighted average of the longitude of the center of the brightness temperature footprints that fall within the EASE grid cell. + + + degrees + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + -180. + + + 179.9989929 + + + -9999. + + + + + + + Diffuse reflecting power of the Earth&apos;s surface used in DCA retrievals within the grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 1. + + + -9999. + + + /Soil_Moisture_Retrieval_Data_PM/albedo_dca_pm + + + + + + + Representative measure of water in the vegetation within the 36 km grid cell. + + + kg/m**2 + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 20. + + + -9999. + + + + + + + 0. + + + Diffuse reflecting power of the Earth&apos;s surface used in SCA-H retrievals within the grid cell. + + + 1. + + + -9999. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + + + + + Horizontal polarization brightness temperature in 36 km Earth grid cell before adjustment for the presence of water bodies. + + + Kelvin + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 330. + + + -9999. + + + + + + + Latitude of the center of the Earth based grid cell. + + + degrees_north + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + -90. + + + 90. + + + -9999. + + + + + + + Vertical polarization brightness temperature in 36 km Earth grid cell adjusted for the presence of water bodies. + + + Kelvin + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 330. + + + -9999. + + + + + + + The measured opacity of the vegetation used in SCA-V retrievals in the grid cell. + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + -999999.875 + + + 999999.875 + + + -9999. + + + + + + + Indicates if the grid point lies on land (0) or water (1). + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0 + + + 1 + + + 65534 + + + + + + + Temperature at land surface based on GMAO GEOS-5 data. + + + Kelvins + + + /Soil_Moisture_Retrieval_Data_AM/latitude /Soil_Moisture_Retrieval_Data_AM/longitude + + + 0. + + + 350. + + + -9999. + + + + diff --git a/tests/data/SC_SPL3SMP_008_prefetch.nc4 b/tests/data/SC_SPL3SMP_008_prefetch.nc4 new file mode 100644 index 0000000000000000000000000000000000000000..f866858c3197c1d0253c0ccbd7f0d4c1d4596a0e GIT binary patch literal 337938 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 Z4NuK*&7hYC!RA{E7>4_>d)<+x{|DTSD{KG& literal 0 HcmV?d00001 diff --git a/tests/unit/test_spatial.py b/tests/unit/test_spatial.py index b77f442..cf83472 100644 --- a/tests/unit/test_spatial.py +++ b/tests/unit/test_spatial.py @@ -202,14 +202,13 @@ def test_get_x_y_index_ranges_from_coordinates( """ smap_varinfo = VarInfoFromDmr( - 'tests/data/SC_SPL3SMP_009.dmr', + 'tests/data/SC_SPL3SMP_008.dmr', 'SPL3SMP', 'hoss/hoss_config.json', ) - smap_file_path = 'tests/data/SC_SPL3SMP_009_prefetch.nc4' + smap_file_path = 'tests/data/SC_SPL3SMP_008_prefetch.nc4' expected_index_ranges = {'projected_x': (487, 595), 'projected_y': (9, 38)} bbox = BBox(2, 54, 42, 72) - smap_variable_name = '/Soil_Moisture_Retrieval_Data_AM/surface_flag' latitude_coordinate = smap_varinfo.get_variable( '/Soil_Moisture_Retrieval_Data_AM/latitude' From 16872b7e5642d715b98809e55a26cb8234a8f762 Mon Sep 17 00:00:00 2001 From: sudhamurthy Date: Thu, 10 Oct 2024 04:10:54 -0400 Subject: [PATCH 35/41] DAS-2232 - added some unit tests and some bug fixes --- hoss/coordinate_utilities.py | 53 ++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/hoss/coordinate_utilities.py b/hoss/coordinate_utilities.py index 1eb53c4..bc5ba14 100644 --- a/hoss/coordinate_utilities.py +++ b/hoss/coordinate_utilities.py @@ -137,10 +137,6 @@ def update_dimension_variables( latitude_coordinate, longitude_coordinate, ) - if not lat_arr.size: - raise MissingCoordinateDataset('latitude') - if not lon_arr.size: - raise MissingCoordinateDataset('longitude') lat_fill, lon_fill = get_fill_values_for_coordinates( latitude_coordinate, longitude_coordinate @@ -192,7 +188,8 @@ def get_row_col_sizes_from_coordinate_datasets( This method returns the row and column sizes of the coordinate datasets """ - + row_size = 0 + col_size = 0 if lat_arr.ndim > 1: col_size = lat_arr.shape[0] row_size = lat_arr.shape[1] @@ -218,7 +215,7 @@ def is_lat_lon_ascending( lat_col = lat_arr[:, 0] lon_row = lon_arr[0, :] - lat_col_valid_indices = get_valid_indices(lon_row, lat_fill, 'latitude') + lat_col_valid_indices = get_valid_indices(lat_col, lat_fill, 'latitude') latitude_ascending = ( lat_col[lat_col_valid_indices[1]] > lat_col[lat_col_valid_indices[0]] ) @@ -240,13 +237,12 @@ def get_lat_lon_arrays( This method is used to return the lat lon arrays from a 2D coordinate dataset. """ - lat_arr = [] - lon_arr = [] - - lat_arr = prefetch_dataset[latitude_coordinate.full_name_path][:] - lon_arr = prefetch_dataset[longitude_coordinate.full_name_path][:] - - return lat_arr, lon_arr + try: + lat_arr = prefetch_dataset[latitude_coordinate.full_name_path][:] + lon_arr = prefetch_dataset[longitude_coordinate.full_name_path][:] + return lat_arr, lon_arr + except Exception as exception: + raise MissingCoordinateDataset('latitude/longitude') from exception def get_geo_grid_corners( @@ -254,7 +250,7 @@ def get_geo_grid_corners( lon_arr: ndarray, lat_fill: float, lon_fill: float, -) -> list[Tuple[float]]: +) -> list[Tuple[float, float]]: """ This method is used to return the lat lon corners from a 2D coordinate dataset. It gets the row and column of the latitude and longitude @@ -295,10 +291,10 @@ def get_geo_grid_corners( max_lat = lat_col[top_right_row_idx] geo_grid_corners = [ - [min_lon, max_lat], - [max_lon, max_lat], - [max_lon, min_lat], - [min_lon, min_lat], + (min_lon, max_lat), + (max_lon, max_lat), + (max_lon, min_lat), + (min_lon, min_lat), ] return geo_grid_corners @@ -309,8 +305,11 @@ def get_valid_indices( """ Returns indices of a valid array without fill values """ + if coordinate_fill: - valid_indices = np.where(coordinate_row_col != coordinate_fill)[0] + valid_indices = np.where( + ~np.isclose(coordinate_row_col, float(coordinate_fill)) + )[0] elif coordinate_name == 'longitude': valid_indices = np.where( (coordinate_row_col >= -180.0) & (coordinate_row_col <= 180.0) @@ -331,16 +330,24 @@ def get_valid_indices( def get_fill_values_for_coordinates( latitude_coordinate: VariableFromDmr, longitude_coordinate: VariableFromDmr, -) -> float | None: +) -> tuple[float | None, float | None]: """ returns fill values for the variable. If it does not exist checks for the overrides from the json file. If there is no overrides, returns None """ - lat_fill_value = latitude_coordinate.get_attribute_value('_fillValue') - lon_fill_value = longitude_coordinate.get_attribute_value('_fillValue') + lat_fill = None + lon_fill = None + lat_fill_value = latitude_coordinate.get_attribute_value('_FillValue') + lon_fill_value = longitude_coordinate.get_attribute_value('_FillValue') # if fill_value is None: # check if there are overrides in hoss_config.json using varinfo # else - return lat_fill_value, lon_fill_value + + if lat_fill_value is not None: + lat_fill = float(lat_fill_value) + if lon_fill_value is not None: + lon_fill = float(lon_fill_value) + + return float(lat_fill), float(lon_fill) From 822758fc17118d4fa9605b2a4d097cefb58cd96f Mon Sep 17 00:00:00 2001 From: sudhamurthy Date: Thu, 10 Oct 2024 04:12:52 -0400 Subject: [PATCH 36/41] DAS-2232 - added some unit tests and some bug fixes --- tests/unit/test_coordinate_utilities.py | 123 ++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 tests/unit/test_coordinate_utilities.py diff --git a/tests/unit/test_coordinate_utilities.py b/tests/unit/test_coordinate_utilities.py new file mode 100644 index 0000000..2612ba3 --- /dev/null +++ b/tests/unit/test_coordinate_utilities.py @@ -0,0 +1,123 @@ +from logging import getLogger +from os.path import exists +from shutil import copy, rmtree +from tempfile import mkdtemp +from unittest import TestCase +from unittest.mock import ANY, patch + +import numpy as np +from harmony.util import config +from netCDF4 import Dataset +from numpy.testing import assert_array_equal +from varinfo import VarInfoFromDmr + +from hoss.coordinate_utilities import ( + get_coordinate_variables, + get_fill_values_for_coordinates, + get_geo_grid_corners, + get_lat_lon_arrays, + get_override_projected_dimension_name, + get_override_projected_dimensions, + get_row_col_sizes_from_coordinate_datasets, + get_valid_indices, + get_variables_with_anonymous_dims, + get_x_y_extents_from_geographic_points, + is_lat_lon_ascending, + update_dimension_variables, +) +from hoss.exceptions import MissingCoordinateDataset + + +class TestCoordinateUtilities(TestCase): + """A class for testing functions in the `hoss.coordinate_utilities` + module. + + """ + + @classmethod + def setUpClass(cls): + """Create fixtures that can be reused for all tests.""" + cls.config = config(validate=False) + cls.logger = getLogger('tests') + cls.varinfo = VarInfoFromDmr( + 'tests/data/SC_SPL3SMP_008.dmr', + 'SPL3SMP', + config_file='hoss/hoss_config.json', + ) + cls.nc4file = 'tests/data/SC_SPL3SMP_008_prefetch.nc4' + cls.latitude = '/Soil_Moisture_Retrieval_Data_AM/latitude' + cls.longitude = '/Soil_Moisture_Retrieval_Data_AM/longitude' + + cls.lon_arr = np.array( + [ + [-179.3, -120.2, -60.6, -9999, -9999, -9999, 80.2, 120.6, 150.5, 178.4], + [-179.3, -120.2, -60.6, -9999, -9999, -9999, 80.2, 120.6, 150.5, 178.4], + [-179.3, -120.2, -60.6, -9999, -9999, -9999, 80.2, 120.6, 150.5, 178.4], + [-179.3, -120.2, -60.6, -9999, -9999, -9999, 80.2, 120.6, 150.5, 178.4], + [-179.3, -120.2, -60.6, -9999, -9999, -9999, 80.2, 120.6, 150.5, 178.4], + ] + ) + + cls.lat_arr = np.array( + [ + [89.3, 89.3, -9999, 89.3, 89.3, 89.3, -9999, 89.3, 89.3, 89.3], + [50.3, 50.3, 50.3, 50.3, 50.3, 50.3, -9999, 50.3, 50.3, 50.3], + [1.3, 1.3, 1.3, 1.3, 1.3, 1.3, -9999, -9999, 1.3, 1.3], + [-9999, -60.2, -60.2, -9999, -9999, -9999, -60.2, -60.2, -60.2, -60.2], + [-88.1, -88.1, -88.1, -9999, -9999, -9999, -88.1, -88.1, -88.1, -88.1], + ] + ) + + def setUp(self): + """Create fixtures that should be unique per test.""" + self.temp_dir = mkdtemp() + + def tearDown(self): + """Remove per-test fixtures.""" + if exists(self.temp_dir): + rmtree(self.temp_dir) + + def test_is_lat_lon_ascending(self): + """Ensure that latitude and longitude values are correctly identified as + ascending or descending. + + """ + + expected_result = False, True + with self.subTest('ascending order even with fill values'): + self.assertEqual( + is_lat_lon_ascending(self.lat_arr, self.lon_arr, -9999, -9999), + expected_result, + ) + + def test_get_geo_grid_corners(self): + """Ensure that the correct corner points returned by the methos + with a set of lat/lon coordinates as input + + """ + prefetch_dataset = Dataset(self.nc4file, 'r+') + lat_fill = -9999.0 + lon_fill = -9999.0 + + # lat_arr = prefetch_dataset[self.latitude][:] + # lon_arr = prefetch_dataset[self.longitude][:] + + expected_geo_corners = [ + (-179.3, 89.3), + (178.4, 89.3), + (178.4, -88.1), + (-179.3, -88.1), + ] + + with self.subTest('Get geo grid corners from coordinates'): + actual_geo_corners = get_geo_grid_corners( + self.lat_arr, + self.lon_arr, + lat_fill, + lon_fill, + ) + for actual, expected in zip(actual_geo_corners, expected_geo_corners): + self.assertAlmostEqual(actual[0], expected[0], places=1) + self.assertAlmostEqual(actual[1], expected[1], places=1) + + prefetch_dataset.close() From 7883465f2d7e045b047ee2f0494f481678bbe2c9 Mon Sep 17 00:00:00 2001 From: sudhamurthy Date: Thu, 10 Oct 2024 12:02:15 -0400 Subject: [PATCH 37/41] DAS-2232 - minor initialization fix --- hoss/coordinate_utilities.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hoss/coordinate_utilities.py b/hoss/coordinate_utilities.py index bc5ba14..ca49327 100644 --- a/hoss/coordinate_utilities.py +++ b/hoss/coordinate_utilities.py @@ -305,7 +305,7 @@ def get_valid_indices( """ Returns indices of a valid array without fill values """ - + valid_indices = np.empty((0, 0)) if coordinate_fill: valid_indices = np.where( ~np.isclose(coordinate_row_col, float(coordinate_fill)) From dd98e81735b1ecef2562a8d768ce3b6ae23e426b Mon Sep 17 00:00:00 2001 From: sudhamurthy Date: Thu, 10 Oct 2024 18:46:48 -0400 Subject: [PATCH 38/41] DAS-2232 - added unit test for get_valid_indices --- tests/unit/test_coordinate_utilities.py | 35 ++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_coordinate_utilities.py b/tests/unit/test_coordinate_utilities.py index 2612ba3..5d5880f 100644 --- a/tests/unit/test_coordinate_utilities.py +++ b/tests/unit/test_coordinate_utilities.py @@ -25,7 +25,7 @@ is_lat_lon_ascending, update_dimension_variables, ) -from hoss.exceptions import MissingCoordinateDataset +from hoss.exceptions import MissingCoordinateDataset, MissingValidCoordinateDataset class TestCoordinateUtilities(TestCase): @@ -77,6 +77,39 @@ def tearDown(self): if exists(self.temp_dir): rmtree(self.temp_dir) + def test_get_valid_indices(self): + """Ensure that latitude and longitude values are correctly identified as + ascending or descending. + + """ + + expected_result1 = np.array([0, 1, 2, 3, 4]) + expected_result2 = np.array([0, 1, 2, 6, 7, 8, 9]) + expected_result3 = np.empty((0, 0)) + + fill_array = np.array([-9999.0, -9999.0, -9999.0, -9999.0]) + with self.subTest('valid indices with no fill values configured'): + actual_result1 = get_valid_indices(self.lat_arr[:, -1], None, 'latitude') + print('actual_result1=' f'{actual_result1}') + print('expected_result1=' f'{expected_result1}') + self.assertTrue(np.array_equal(actual_result1, expected_result1)) + + with self.subTest('valid indices with fill values configured'): + actual_result2 = get_valid_indices(self.lon_arr[0, :], -9999.0, 'longitude') + print('actual_result2=' f'{actual_result2}') + print('expected_result2=' f'{expected_result2}') + self.assertTrue(np.array_equal(actual_result2, expected_result2)) + + with self.subTest('all fill values - no valid indices'): + with self.assertRaises(MissingValidCoordinateDataset) as context: + actual_result3 = get_valid_indices(fill_array, -9999.0, 'longitude') + print('actual_result3=' f'{actual_result3}') + print('expected_result3=' f'{expected_result3}') + self.assertEqual( + context.exception.message, + 'Coordinate: "longitude" is not valid in source granule file.', + ) + def test_is_lat_lon_ascending(self): """Ensure that latitude and longitude values are correctly identified as ascending or descending. From de350b62112b7fef73f2184a60196c25368ccd12 Mon Sep 17 00:00:00 2001 From: sudhamurthy Date: Wed, 16 Oct 2024 15:52:18 -0400 Subject: [PATCH 39/41] DAS-2232 - replaced get_geo_grid_corners method to get 2 valid geo grid points --- hoss/coordinate_utilities.py | 297 +++++++++++++++--------- hoss/exceptions.py | 37 ++- hoss/hoss_config.json | 119 +++++----- tests/unit/test_coordinate_utilities.py | 91 +++----- 4 files changed, 294 insertions(+), 250 deletions(-) diff --git a/hoss/coordinate_utilities.py b/hoss/coordinate_utilities.py index ca49327..9223145 100644 --- a/hoss/coordinate_utilities.py +++ b/hoss/coordinate_utilities.py @@ -8,16 +8,14 @@ import numpy as np from netCDF4 import Dataset from numpy import ndarray -from pyproj import CRS +from pyproj import CRS, Transformer from varinfo import VariableFromDmr, VarInfoFromDmr from hoss.exceptions import ( - IrregularCoordinateDatasets, - MissingCoordinateDataset, - MissingValidCoordinateDataset, -) -from hoss.projection_utilities import ( - get_x_y_extents_from_geographic_points, + CannotComputeDimensionResolution, + InvalidCoordinateVariable, + IrregularCoordinateVariables, + MissingCoordinateVariable, ) @@ -74,16 +72,16 @@ def get_override_projected_dimensions( def get_variables_with_anonymous_dims( - varinfo: VarInfoFromDmr, required_variables: set[str] -) -> bool: + varinfo: VarInfoFromDmr, variables: set[str] +) -> set[str]: """ - returns the list of required variables without any + returns a set of variables without any dimensions """ return set( - required_variable - for required_variable in required_variables - if len(varinfo.get_variable(required_variable).dimensions) == 0 + variable + for variable in variables + if len(varinfo.get_variable(variable).dimensions) == 0 ) @@ -146,36 +144,19 @@ def update_dimension_variables( lat_arr, lon_arr, ) - - geo_grid_corners = get_geo_grid_corners( - lat_arr, - lon_arr, - lat_fill, - lon_fill, + geo_grid_points = get_two_valid_geo_grid_points( + lat_arr, lon_arr, lat_fill, lon_fill, row_size, col_size ) - x_y_extents = get_x_y_extents_from_geographic_points(geo_grid_corners, crs) + x_y_values = get_x_y_values_from_geographic_points(geo_grid_points, crs) - # get grid size and resolution - x_min = x_y_extents['x_min'] - x_max = x_y_extents['x_max'] - y_min = x_y_extents['y_min'] - y_max = x_y_extents['y_max'] - x_resolution = (x_max - x_min) / row_size - y_resolution = (y_max - y_min) / col_size + row_indices, col_indices = zip(*list(x_y_values.keys())) - # create the xy dim scales - lat_asc, lon_asc = is_lat_lon_ascending(lat_arr, lon_arr, lat_fill, lon_fill) + x_values, y_values = zip(*list(x_y_values.values())) - if lon_asc: - x_dim = np.arange(x_min, x_max, x_resolution) - else: - x_dim = np.arange(x_min, x_max, -x_resolution) + y_dim = get_dimension_scale_from_dimvalues(y_values, row_indices, row_size) - if lat_asc: - y_dim = np.arange(y_max, y_min, y_resolution) - else: - y_dim = np.arange(y_max, y_min, -y_resolution) + x_dim = get_dimension_scale_from_dimvalues(x_values, col_indices, col_size) return {'projected_y': y_dim, 'projected_x': x_dim} @@ -190,44 +171,17 @@ def get_row_col_sizes_from_coordinate_datasets( """ row_size = 0 col_size = 0 - if lat_arr.ndim > 1: + if lat_arr.ndim > 1 and lon_arr.shape == lat_arr.shape: col_size = lat_arr.shape[0] row_size = lat_arr.shape[1] - if (lon_arr.shape[0] != lat_arr.shape[0]) or (lon_arr.shape[1] != lat_arr.shape[1]): - raise IrregularCoordinateDatasets(lon_arr.shape, lat_arr.shape) - if lat_arr.ndim and lon_arr.ndim == 1: + elif lat_arr.ndim == 1 and lon_arr.ndim == 1: col_size = lat_arr.size row_size = lon_arr.size + elif lon_arr.shape != lat_arr.shape: + raise IrregularCoordinateVariables(lon_arr.ndim, lat_arr.ndim) return row_size, col_size -def is_lat_lon_ascending( - lat_arr: ndarray, - lon_arr: ndarray, - lat_fill: float, - lon_fill: float, -) -> tuple[bool, bool]: - """ - Checks if the latitude and longitude cooordinate datasets have values - that are ascending - """ - - lat_col = lat_arr[:, 0] - lon_row = lon_arr[0, :] - - lat_col_valid_indices = get_valid_indices(lat_col, lat_fill, 'latitude') - latitude_ascending = ( - lat_col[lat_col_valid_indices[1]] > lat_col[lat_col_valid_indices[0]] - ) - - lon_row_valid_indices = get_valid_indices(lon_row, lon_fill, 'longitude') - longitude_ascending = ( - lon_row[lon_row_valid_indices[1]] > lon_row[lon_row_valid_indices[0]] - ) - - return latitude_ascending, longitude_ascending - - def get_lat_lon_arrays( prefetch_dataset: Dataset, latitude_coordinate: VariableFromDmr, @@ -242,61 +196,184 @@ def get_lat_lon_arrays( lon_arr = prefetch_dataset[longitude_coordinate.full_name_path][:] return lat_arr, lon_arr except Exception as exception: - raise MissingCoordinateDataset('latitude/longitude') from exception + raise MissingCoordinateVariable('latitude/longitude') from exception -def get_geo_grid_corners( +def get_two_valid_geo_grid_points( lat_arr: ndarray, lon_arr: ndarray, lat_fill: float, lon_fill: float, -) -> list[Tuple[float, float]]: + row_size: float, + col_size: float, +) -> dict[int, tuple]: """ - This method is used to return the lat lon corners from a 2D + This method is used to return two valid lat lon points from a 2D coordinate dataset. It gets the row and column of the latitude and longitude - arrays to get the corner points. This does a check for fill values and - This method does not check if there are fill values in the corner points - to go down to the next row and col. The fill values in the corner points - still needs to be addressed. It will raise an exception in those - cases. + arrays to get two valid points. This does a check for fill values and + This method does not go down to the next row and col. if the selected row and + column all have fills, it will raise an exception in those cases. + """ + first_row_col_index = -1 + first_row_row_index = 0 + next_col_row_index = -1 + next_col_col_index = 1 + lat_row_valid_indices = lon_row_valid_indices = np.empty((0, 0)) + + # get the first row with points that are valid in the lat and lon rows + first_row_row_index, lat_row_valid_indices = get_valid_indices_in_dataset( + lat_arr, row_size, lat_fill, 'latitude', 'row', first_row_row_index + ) + first_row_row_index1, lon_row_valid_indices = get_valid_indices_in_dataset( + lon_arr, row_size, lon_fill, 'longitude', 'row', first_row_row_index + ) + # get a point that is common on both row datasets + if ( + (first_row_row_index == first_row_row_index1) + and (lat_row_valid_indices.size > 0) + and (lon_row_valid_indices.size > 0) + ): + first_row_col_index = np.intersect1d( + lat_row_valid_indices, lon_row_valid_indices + )[0] + + # get a valid column from the latitude and longitude datasets + next_col_col_index, lon_col_valid_indices = get_valid_indices_in_dataset( + lon_arr, col_size, lon_fill, 'longitude', 'col', next_col_col_index + ) + next_col_col_index1, lat_col_valid_indices = get_valid_indices_in_dataset( + lat_arr, col_size, lat_fill, 'latitude', 'col', next_col_col_index + ) + + # get a point that is common to both column datasets + if ( + (next_col_col_index == next_col_col_index1) + and (lat_col_valid_indices.size > 0) + and (lon_col_valid_indices.size > 0) + ): + next_col_row_index = np.intersect1d( + lat_col_valid_indices, lon_col_valid_indices + )[-1] + + # if the whole row and whole column has no valid indices + # we throw an exception now. This can be extended to move + # to the next row/col + if first_row_col_index == -1: + raise InvalidCoordinateVariable('latitude/longitude') + if next_col_row_index == -1: + raise InvalidCoordinateVariable('latitude/longitude') + + geo_grid_indexes = [ + (first_row_row_index, first_row_col_index), + (next_col_row_index, next_col_col_index), + ] + + geo_grid_points = [ + ( + lon_arr[first_row_row_index][first_row_col_index], + lat_arr[first_row_row_index][first_row_col_index], + ), + ( + lon_arr[next_col_row_index][next_col_col_index], + lat_arr[next_col_row_index][next_col_col_index], + ), + ] + + return { + geo_grid_indexes[0]: geo_grid_points[0], + geo_grid_indexes[1]: geo_grid_points[1], + } + + +def get_x_y_values_from_geographic_points(points: Dict, crs: CRS) -> Dict[tuple, tuple]: + """Take an input list of (longitude, latitude) coordinates and project + those points to the target grid. Then return the x-y dimscales + """ + point_longitudes, point_latitudes = zip(*list(points.values())) - top_left_row_idx = 0 - top_left_col_idx = 0 + from_geo_transformer = Transformer.from_crs(4326, crs) + points_x, points_y = ( # pylint: disable=unpacking-non-sequence + from_geo_transformer.transform(point_latitudes, point_longitudes) + ) - # get the first row from the longitude dataset - lon_row = lon_arr[top_left_row_idx, :] - lon_row_valid_indices = get_valid_indices(lon_row, lon_fill, 'longitude') + x_y_points = {} + for index, point_x, point_y in zip(list(points.keys()), points_x, points_y): + x_y_points.update({index: (point_x, point_y)}) - # get the index of the minimum longitude after checking for invalid entries - top_left_col_idx = lon_row_valid_indices[lon_row[lon_row_valid_indices].argmin()] - min_lon = lon_row[top_left_col_idx] + return x_y_points - # get the index of the maximum longitude after checking for invalid entries - top_right_col_idx = lon_row_valid_indices[lon_row[lon_row_valid_indices].argmax()] - max_lon = lon_row[top_right_col_idx] - # get the last valid longitude column to get the latitude array - lat_col = lat_arr[:, top_right_col_idx] - lat_col_valid_indices = get_valid_indices(lat_col, lat_fill, 'latitude') +def get_dimension_scale_from_dimvalues( + dim_values: ndarray, dim_indices: ndarray, dim_size: float +) -> ndarray: + """ + return a full dimension scale based on the 2 projected points and + grid size + """ + dim_resolution = 0.0 + if (dim_indices[1] != dim_indices[0]) and (dim_values[1] != dim_values[0]): + dim_resolution = (dim_values[1] - dim_values[0]) / ( + dim_indices[1] - dim_indices[0] + ) + if dim_resolution == 0.0: + raise CannotComputeDimensionResolution(dim_values[0], dim_indices[0]) - # get the index of minimum latitude after checking for valid values - bottom_right_row_idx = lat_col_valid_indices[ - lat_col[lat_col_valid_indices].argmin() - ] - min_lat = lat_col[bottom_right_row_idx] + # create the dim scale + dim_asc = dim_values[1] > dim_values[0] - # get the index of maximum latitude after checking for valid values - top_right_row_idx = lat_col_valid_indices[lat_col[lat_col_valid_indices].argmax()] - max_lat = lat_col[top_right_row_idx] + if dim_asc: + dim_min = dim_values[0] + (dim_resolution * dim_indices[0]) + dim_max = dim_values[0] + (dim_resolution * (dim_size - dim_indices[0] - 1)) + dim_data = np.linspace(dim_min, dim_max, dim_size) + else: + dim_max = dim_values[0] + (dim_resolution * dim_indices[0]) + dim_min = dim_values[0] - (-dim_resolution * (dim_size - dim_indices[0] - 1)) + dim_data = np.linspace(dim_max, dim_min, dim_size) - geo_grid_corners = [ - (min_lon, max_lat), - (max_lon, max_lat), - (max_lon, min_lat), - (min_lon, min_lat), - ] - return geo_grid_corners + return dim_data + + +def get_valid_indices_in_dataset( + coordinate_arr: ndarray, + dim_size: int, + coordinate_fill: float, + coordinate_name: str, + span_type: str, + start_index: int, +) -> tuple[int, ndarray]: + """ + This method gets valid indices in a row or column of a + coordinate dataset + """ + coordinate_index = start_index + valid_indices = [] + if span_type == 'row': + valid_indices = get_valid_indices( + coordinate_arr[coordinate_index, :], coordinate_fill, coordinate_name + ) + else: + valid_indices = get_valid_indices( + coordinate_arr[:, coordinate_index], coordinate_fill, coordinate_name + ) + while valid_indices.size == 0: + if coordinate_index < dim_size: + coordinate_index = coordinate_index + 1 + if span_type == 'row': + valid_indices = get_valid_indices( + coordinate_arr[coordinate_index, :], + coordinate_fill, + coordinate_name, + ) + else: + valid_indices = get_valid_indices( + coordinate_arr[:, coordinate_index], + coordinate_fill, + coordinate_name, + ) + else: + raise InvalidCoordinateVariable(coordinate_name) + return coordinate_index, valid_indices def get_valid_indices( @@ -319,11 +396,6 @@ def get_valid_indices( (coordinate_row_col >= -90.0) & (coordinate_row_col <= 90.0) )[0] - # if the first row does not have valid indices, - # should go down to the next row. We throw an exception - # for now till that gets addressed - if not valid_indices.size: - raise MissingValidCoordinateDataset(coordinate_name) return valid_indices @@ -341,9 +413,6 @@ def get_fill_values_for_coordinates( lon_fill = None lat_fill_value = latitude_coordinate.get_attribute_value('_FillValue') lon_fill_value = longitude_coordinate.get_attribute_value('_FillValue') - # if fill_value is None: - # check if there are overrides in hoss_config.json using varinfo - # else if lat_fill_value is not None: lat_fill = float(lat_fill_value) diff --git a/hoss/exceptions.py b/hoss/exceptions.py index 16792b0..25de20a 100644 --- a/hoss/exceptions.py +++ b/hoss/exceptions.py @@ -108,48 +108,61 @@ def __init__(self): ) -class MissingCoordinateDataset(CustomError): +class MissingCoordinateVariable(CustomError): """This exception is raised when HOSS tries to get latitude and longitude - datasets and they are missing or empty. These datasets are referred to + variables and they are missing or empty. These variables are referred to in the science variables with coordinate attributes. """ def __init__(self, referring_variable): super().__init__( - 'MissingCoordinateDataset', + 'MissingCoordinateVariable', f'Coordinate: "{referring_variable}" is ' 'not present in source granule file.', ) -class MissingValidCoordinateDataset(CustomError): +class InvalidCoordinateVariable(CustomError): """This exception is raised when HOSS tries to get latitude and longitude - datasets and they have fill values to the extent that it cannot be used. - These datasets are referred in the science variables with coordinate attributes. + variables and they have fill values to the extent that it cannot be used. + These variables are referred in the science variables with coordinate attributes. """ def __init__(self, referring_variable): super().__init__( - 'MissingValidCoordinateDataset', + 'InvalidCoordinateVariable', f'Coordinate: "{referring_variable}" is ' 'not valid in source granule file.', ) -class IrregularCoordinateDatasets(CustomError): +class IrregularCoordinateVariables(CustomError): """This exception is raised when HOSS tries to get latitude and longitude - datasets and they are missing or empty. These datasets are referred to + coordinate variable and they are missing or empty. These variables are referred to in the science variables with coordinate attributes. """ def __init__(self, longitude_shape, latitude_shape): super().__init__( - 'IrregularCoordinateDatasets', - f'Longitude dataset shape: "{longitude_shape}"' - f'does not match the latitude dataset shape: "{latitude_shape}"', + 'IrregularCoordinateVariables', + f'Longitude coordinate shape: "{longitude_shape}"' + f'does not match the latitude coordinate shape: "{latitude_shape}"', + ) + + +class CannotComputeDimensionResolution(CustomError): + """This exception is raised when the two values passed to + the method computing the resolution are equal + + """ + + def __init__(self, dim_value, dim_index): + super().__init__( + 'CannotComputeDimensionResolution', + f'dim_value: "{dim_value}" dim_index: "{dim_index}"', ) diff --git a/hoss/hoss_config.json b/hoss/hoss_config.json index 70e148f..9c75f80 100644 --- a/hoss/hoss_config.json +++ b/hoss/hoss_config.json @@ -143,67 +143,58 @@ { "Applicability": { "Mission": "SMAP", - "ShortNamePath": "SPL3FT(P|P_E)" + "ShortNamePath": "SPL3FT(P|P_E)", + "Variable_Pattern": "(?i).*global.*" }, - "Applicability_Group": [ + "Attributes": [ { - "Applicability": { - "Variable_Pattern": "(?i).*global.*" - }, - "Attributes": [ - { - "Name": "grid_mapping", - "Value": "/EASE2_global_projection" - } - ], - "_Description": "SMAP L3 collections omit global grid mapping information" - }, + "Name": "grid_mapping", + "Value": "/EASE2_global_projection" + } + ], + "_Description": "SMAP L3 collections omit global grid mapping information" + }, + { + "Applicability": { + "Mission": "SMAP", + "ShortNamePath": "SPL3FT(P|P_E)", + "Variable_Pattern": "(?i).*polar.*" + }, + "Attributes": [ { - "Applicability": { - "Variable_Pattern": "(?i).*polar.*" - }, - "Attributes": [ - { - "Name": "grid_mapping", - "Value": "/EASE2_polar_projection" - } - ], - "_Description": "SMAP L3 collections omit polar grid mapping information" + "Name": "grid_mapping", + "Value": "/EASE2_polar_projection" } - ] + ], + "_Description": "SMAP L3 collections omit polar grid mapping information" }, { "Applicability": { "Mission": "SMAP", - "ShortNamePath": "SPL3SMP_E" + "ShortNamePath": "SPL3SMP_E", + "Variable_Pattern": "Soil_Moisture_Retrieval_Data_(A|P)M/.*" }, - "Applicability_Group": [ - { - "Applicability": { - "Variable_Pattern": "Soil_Moisture_Retrieval_Data_(A|P)M/.*" - }, - "Attributes": [ - { - "Name": "grid_mapping", - "Value": "/EASE2_global_projection" - } - ], - "_Description": "SMAP L3 collections omit global grid mapping information" - }, + "Attributes": [ { - "Applicability": { + "Name": "grid_mapping", + "Value": "/EASE2_global_projection" + } + ], + "_Description": "SMAP L3 collections omit global grid mapping information" + }, + { + "Applicability": { + "Mission": "SMAP", + "ShortNamePath": "SPL3SMP_E", "Variable_Pattern": "Soil_Moisture_Retrieval_Data_Polar_(A|P)M/.*" - }, - "Attributes": [ - { - "Name": "grid_mapping", - "Value": "/EASE2_polar_projection" - } - ], - "_Description": "SMAP L3 collections omit polar grid mapping information" - + }, + "Attributes": [ + { + "Name": "grid_mapping", + "Value": "/EASE2_polar_projection" } - ] + ], + "_Description": "SMAP L3 collections omit polar grid mapping information" }, { "Applicability": { @@ -234,14 +225,10 @@ { "Applicability": { "Mission": "SMAP", - "ShortNamePath": "SPL3FT(P|P_E)|SPL3SM(P|P_E|A|AP)|SPL2SMAP_S" + "ShortNamePath": "SPL3FT(P|P_E)|SPL3SM(P|P_E|A|AP)|SPL2SMAP_S", + "Variable_Pattern": "/EASE2_global_projection" }, - "Applicability_Group": [ - { - "Applicability": { - "Variable_Pattern": "/EASE2_global_projection" - }, - "Attributes": [ + "Attributes": [ { "Name": "grid_mapping_name", "Value": "lambert_cylindrical_equal_area" @@ -264,12 +251,14 @@ } ], "_Description": "Provide missing global grid mapping attributes for SMAP L3 collections." - }, - { - "Applicability": { + }, + { + "Applicability": { + "Mission": "SMAP", + "ShortNamePath": "SPL3FT(P|P_E)|SPL3SM(P|P_E|A|AP)|SPL2SMAP_S", "Variable_Pattern": "/EASE2_polar_projection" - }, - "Attributes": [ + }, + "Attributes": [ { "Name": "grid_mapping_name", "Value": "lambert_azimuthal_equal_area" @@ -289,11 +278,9 @@ "Name": "false_northing", "Value": 0.0 } - ], - "_Description": "Provide missing polar grid mapping attributes for SMAP L3 collections." - } - ] - }, + ], + "_Description": "Provide missing polar grid mapping attributes for SMAP L3 collections." + }, { "Applicability": { "Mission": "SMAP", diff --git a/tests/unit/test_coordinate_utilities.py b/tests/unit/test_coordinate_utilities.py index 5d5880f..dc651e5 100644 --- a/tests/unit/test_coordinate_utilities.py +++ b/tests/unit/test_coordinate_utilities.py @@ -14,18 +14,16 @@ from hoss.coordinate_utilities import ( get_coordinate_variables, get_fill_values_for_coordinates, - get_geo_grid_corners, get_lat_lon_arrays, get_override_projected_dimension_name, get_override_projected_dimensions, get_row_col_sizes_from_coordinate_datasets, + get_two_valid_geo_grid_points, get_valid_indices, get_variables_with_anonymous_dims, - get_x_y_extents_from_geographic_points, - is_lat_lon_ascending, update_dimension_variables, ) -from hoss.exceptions import MissingCoordinateDataset, MissingValidCoordinateDataset +from hoss.exceptions import InvalidCoordinateVariable, MissingCoordinateVariable class TestCoordinateUtilities(TestCase): @@ -83,74 +81,51 @@ def test_get_valid_indices(self): """ - expected_result1 = np.array([0, 1, 2, 3, 4]) - expected_result2 = np.array([0, 1, 2, 6, 7, 8, 9]) - expected_result3 = np.empty((0, 0)) + expected_valid_indices_lat_arr = np.array([0, 1, 2, 3, 4]) + expected_valid_indices_lon_arr = np.array([0, 1, 2, 6, 7, 8, 9]) fill_array = np.array([-9999.0, -9999.0, -9999.0, -9999.0]) with self.subTest('valid indices with no fill values configured'): - actual_result1 = get_valid_indices(self.lat_arr[:, -1], None, 'latitude') - print('actual_result1=' f'{actual_result1}') - print('expected_result1=' f'{expected_result1}') - self.assertTrue(np.array_equal(actual_result1, expected_result1)) + valid_indices_lat_arr = get_valid_indices( + self.lat_arr[:, -1], None, 'latitude' + ) + self.assertTrue( + np.array_equal(valid_indices_lat_arr, expected_valid_indices_lat_arr) + ) with self.subTest('valid indices with fill values configured'): - actual_result2 = get_valid_indices(self.lon_arr[0, :], -9999.0, 'longitude') - print('actual_result2=' f'{actual_result2}') - print('expected_result2=' f'{expected_result2}') - self.assertTrue(np.array_equal(actual_result2, expected_result2)) + valid_indices_lon_arr = get_valid_indices( + self.lon_arr[0, :], -9999.0, 'longitude' + ) + self.assertTrue( + np.array_equal(valid_indices_lon_arr, expected_valid_indices_lon_arr) + ) with self.subTest('all fill values - no valid indices'): - with self.assertRaises(MissingValidCoordinateDataset) as context: - actual_result3 = get_valid_indices(fill_array, -9999.0, 'longitude') - print('actual_result3=' f'{actual_result3}') - print('expected_result3=' f'{expected_result3}') - self.assertEqual( - context.exception.message, - 'Coordinate: "longitude" is not valid in source granule file.', - ) - - def test_is_lat_lon_ascending(self): - """Ensure that latitude and longitude values are correctly identified as - ascending or descending. + valid_indices = get_valid_indices(fill_array, -9999.0, 'longitude') + self.assertTrue(valid_indices.size == 0) - """ - - expected_result = False, True - with self.subTest('ascending order even with fill values'): - self.assertEqual( - is_lat_lon_ascending(self.lat_arr, self.lon_arr, -9999, -9999), - expected_result, - ) - - def test_get_geo_grid_corners(self): - """Ensure that the correct corner points returned by the methos + def test_get_two_valid_geo_grid_points(self): + """Ensure that two valid lat/lon points returned by the method with a set of lat/lon coordinates as input """ - prefetch_dataset = Dataset(self.nc4file, 'r+') + prefetch_dataset = Dataset(self.nc4file, 'r') lat_fill = -9999.0 lon_fill = -9999.0 + row_size = 406 + col_size = 964 + + expected_geo_grid_points = [(-179.3, 89.3), (-120.2, -88.1)] - # lat_arr = prefetch_dataset[self.latitude][:] - # lon_arr = prefetch_dataset[self.longitude][:] - - expected_geo_corners = [ - (-179.3, 89.3), - (178.4, 89.3), - (178.4, -88.1), - (-179.3, -88.1), - ] - - with self.subTest('Get geo grid corners from coordinates'): - actual_geo_corners = get_geo_grid_corners( - self.lat_arr, - self.lon_arr, - lat_fill, - lon_fill, + with self.subTest('Get two valid geo grid points from coordinates'): + actual_geo_grid_points = get_two_valid_geo_grid_points( + self.lat_arr, self.lon_arr, lat_fill, lon_fill, row_size, col_size ) - for actual, expected in zip(actual_geo_corners, expected_geo_corners): - self.assertAlmostEqual(actual[0], expected[0], places=1) - self.assertAlmostEqual(actual[1], expected[1], places=1) + for actual, expected in zip( + actual_geo_grid_points.values(), expected_geo_grid_points + ): + self.assertEqual(actual[0], expected[0]) + self.assertEqual(actual[1], expected[1]) prefetch_dataset.close() From 0666248f0d96e91bf8a900c598be18b674b1cfa9 Mon Sep 17 00:00:00 2001 From: sudhamurthy Date: Wed, 16 Oct 2024 16:43:20 -0400 Subject: [PATCH 40/41] DAS-2232 - minor name from dataset to variable --- hoss/spatial.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/hoss/spatial.py b/hoss/spatial.py index 7b861f4..60eda03 100644 --- a/hoss/spatial.py +++ b/hoss/spatial.py @@ -247,7 +247,7 @@ def get_x_y_index_ranges_from_coordinates( crs = get_variable_crs(non_spatial_variable, varinfo) projected_x = 'projected_x' projected_y = 'projected_y' - override_dimension_datasets = update_dimension_variables( + override_dimension_variables = update_dimension_variables( prefetch_coordinate_datasets, latitude_coordinate, longitude_coordinate, @@ -257,21 +257,21 @@ def get_x_y_index_ranges_from_coordinates( if not set((projected_x, projected_y)).issubset(set(index_ranges.keys())): x_y_extents = get_projected_x_y_extents( - override_dimension_datasets[projected_x][:], - override_dimension_datasets[projected_y][:], + override_dimension_variables[projected_x][:], + override_dimension_variables[projected_y][:], crs, shape_file=shape_file_path, bounding_box=bounding_box, ) x_index_ranges = get_dimension_index_range( - override_dimension_datasets[projected_x][:], + override_dimension_variables[projected_x][:], x_y_extents['x_min'], x_y_extents['x_max'], bounds_values=None, ) y_index_ranges = get_dimension_index_range( - override_dimension_datasets[projected_y][:], + override_dimension_variables[projected_y][:], x_y_extents['y_min'], x_y_extents['y_max'], bounds_values=None, From e07099a14f30958a3b22ec194349df7209d74be8 Mon Sep 17 00:00:00 2001 From: sudhamurthy Date: Thu, 17 Oct 2024 04:49:04 -0400 Subject: [PATCH 41/41] DAS-2232 - added unit tests --- hoss/coordinate_utilities.py | 9 +- hoss/exceptions.py | 1 + tests/unit/test_coordinate_utilities.py | 158 ++++++++++++++++++++++-- 3 files changed, 154 insertions(+), 14 deletions(-) diff --git a/hoss/coordinate_utilities.py b/hoss/coordinate_utilities.py index 9223145..ae5bfbe 100644 --- a/hoss/coordinate_utilities.py +++ b/hoss/coordinate_utilities.py @@ -31,12 +31,13 @@ def get_override_projected_dimension_name( """ override_variable = varinfo.get_variable(variable_name) - projected_dimension_name = '' if override_variable is not None: if override_variable.is_latitude(): projected_dimension_name = 'projected_y' elif override_variable.is_longitude(): projected_dimension_name = 'projected_x' + else: + projected_dimension_name = '' return projected_dimension_name @@ -327,7 +328,7 @@ def get_dimension_scale_from_dimvalues( dim_max = dim_values[0] + (dim_resolution * (dim_size - dim_indices[0] - 1)) dim_data = np.linspace(dim_min, dim_max, dim_size) else: - dim_max = dim_values[0] + (dim_resolution * dim_indices[0]) + dim_max = dim_values[0] + (-dim_resolution * dim_indices[0]) dim_min = dim_values[0] - (-dim_resolution * (dim_size - dim_indices[0] - 1)) dim_data = np.linspace(dim_max, dim_min, dim_size) @@ -382,7 +383,7 @@ def get_valid_indices( """ Returns indices of a valid array without fill values """ - valid_indices = np.empty((0, 0)) + if coordinate_fill: valid_indices = np.where( ~np.isclose(coordinate_row_col, float(coordinate_fill)) @@ -395,6 +396,8 @@ def get_valid_indices( valid_indices = np.where( (coordinate_row_col >= -90.0) & (coordinate_row_col <= 90.0) )[0] + else: + valid_indices = np.empty((0, 0)) return valid_indices diff --git a/hoss/exceptions.py b/hoss/exceptions.py index 25de20a..574f27f 100644 --- a/hoss/exceptions.py +++ b/hoss/exceptions.py @@ -162,6 +162,7 @@ class CannotComputeDimensionResolution(CustomError): def __init__(self, dim_value, dim_index): super().__init__( 'CannotComputeDimensionResolution', + 'Cannot compute the dimension resolution for ' f'dim_value: "{dim_value}" dim_index: "{dim_index}"', ) diff --git a/tests/unit/test_coordinate_utilities.py b/tests/unit/test_coordinate_utilities.py index dc651e5..56e9ad9 100644 --- a/tests/unit/test_coordinate_utilities.py +++ b/tests/unit/test_coordinate_utilities.py @@ -13,6 +13,7 @@ from hoss.coordinate_utilities import ( get_coordinate_variables, + get_dimension_scale_from_dimvalues, get_fill_values_for_coordinates, get_lat_lon_arrays, get_override_projected_dimension_name, @@ -20,10 +21,16 @@ get_row_col_sizes_from_coordinate_datasets, get_two_valid_geo_grid_points, get_valid_indices, + get_valid_indices_in_dataset, get_variables_with_anonymous_dims, + get_x_y_values_from_geographic_points, update_dimension_variables, ) -from hoss.exceptions import InvalidCoordinateVariable, MissingCoordinateVariable +from hoss.exceptions import ( + CannotComputeDimensionResolution, + InvalidCoordinateVariable, + MissingCoordinateVariable, +) class TestCoordinateUtilities(TestCase): @@ -66,21 +73,135 @@ def setUpClass(cls): ] ) + cls.lon_arr_reversed = np.array( + [ + [ + -179.3, + -179.3, + -179.3, + -179.3, + -9999, + -9999, + -179.3, + -179.3, + -179.3, + -179.3, + ], + [ + -120.2, + -120.2, + -120.2, + -9999, + -9999, + -120.2, + -120.2, + -120.2, + -120.2, + -120.2, + ], + [20.6, 20.6, 20.6, 20.6, 20.6, 20.6, 20.6, 20.6, -9999, -9999], + [150.5, 150.5, 150.5, 150.5, 150.5, 150.5, -9999, -9999, 150.5, 150.5], + [178.4, 178.4, 178.4, 178.4, 178.4, 178.4, 178.4, -9999, 178.4, 178.4], + ] + ) + + cls.lat_arr_reversed = np.array( + [ + [89.3, 79.3, -9999, 59.3, 29.3, 2.1, -9999, -59.3, -79.3, -89.3], + [89.3, 79.3, 60.3, 59.3, 29.3, 2.1, -9999, -59.3, -79.3, -89.3], + [89.3, -9999, 60.3, 59.3, 29.3, 2.1, -9999, -9999, -9999, -89.3], + [-9999, 79.3, -60.3, -9999, -9999, -9999, -60.2, -59.3, -79.3, -89.3], + [-89.3, 79.3, -60.3, -9999, -9999, -9999, -60.2, -59.3, -79.3, -9999], + ] + ) + def setUp(self): """Create fixtures that should be unique per test.""" - self.temp_dir = mkdtemp() def tearDown(self): """Remove per-test fixtures.""" - if exists(self.temp_dir): - rmtree(self.temp_dir) + + def get_coordinate_variables(self): + """Ensure that the correct coordinate variables are + retrieved for the reqquested science variable + + """ + + requested_science_variables = set( + '/Soil_Moisture_Retrieval_Data_AM/surface_flag', + '/Soil_Moisture_Retrieval_Data_AM/landcover_class', + '/Soil_Moisture_Retrieval_Data_PM/surface_flag_pm', + ) + + expected_coordinate_variables = tuple([self.latitude], [self.longitude]) + + with self.subTest('Retrieves expected coordinates for the requested variable'): + self.assertTupleEqual( + get_coordinate_variables(self.varinfo, requested_science_variables), + expected_coordinate_variables, + ) + + def test_get_dimension_scale_from_dimvalues(self): + """Ensure that the dimension scale generated from the + provided dimension values are accurate for ascending and + descending scales + """ + + dim_values_asc = np.array([2, 4]) + dim_indices_asc = np.array([0, 1]) + dim_size_asc = 12 + expected_dim_asc = np.array([2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24]) + + dim_values_desc = np.array([100, 70]) + dim_indices_desc = np.array([2, 5]) + dim_size_desc = 10 + expected_dim_desc = np.array([120, 110, 100, 90, 80, 70, 60, 50, 40, 30]) + + dim_values_invalid = np.array([2, 2]) + dim_indices_asc = np.array([0, 1]) + dim_size_asc = 12 + + dim_values_desc = np.array([100, 70]) + dim_indices_invalid = np.array([5, 5]) + dim_size_desc = 10 + + with self.subTest('valid ascending dim scale'): + dim_scale_values = get_dimension_scale_from_dimvalues( + dim_values_asc, dim_indices_asc, dim_size_asc + ) + self.assertTrue(np.array_equal(dim_scale_values, expected_dim_asc)) + with self.subTest('valid descending dim scale'): + dim_scale_values = get_dimension_scale_from_dimvalues( + dim_values_desc, dim_indices_desc, dim_size_desc + ) + self.assertTrue(np.array_equal(dim_scale_values, expected_dim_desc)) + + with self.subTest('invalid dimension values'): + with self.assertRaises(CannotComputeDimensionResolution) as context: + get_dimension_scale_from_dimvalues( + dim_values_invalid, dim_indices_asc, dim_size_asc + ) + self.assertEqual( + context.exception.message, + 'Cannot compute the dimension resolution for ' + 'dim_value: "2" dim_index: "0"', + ) + with self.subTest('invalid dimension indices'): + with self.assertRaises(CannotComputeDimensionResolution) as context: + get_dimension_scale_from_dimvalues( + dim_values_desc, dim_indices_invalid, dim_size_desc + ) + self.assertEqual( + context.exception.message, + 'Cannot compute the dimension resolution for ' + 'dim_value: "100" dim_index: "5"', + ) def test_get_valid_indices(self): """Ensure that latitude and longitude values are correctly identified as ascending or descending. """ - expected_valid_indices_lat_arr = np.array([0, 1, 2, 3, 4]) expected_valid_indices_lon_arr = np.array([0, 1, 2, 6, 7, 8, 9]) @@ -110,22 +231,37 @@ def test_get_two_valid_geo_grid_points(self): with a set of lat/lon coordinates as input """ - prefetch_dataset = Dataset(self.nc4file, 'r') lat_fill = -9999.0 lon_fill = -9999.0 row_size = 406 col_size = 964 expected_geo_grid_points = [(-179.3, 89.3), (-120.2, -88.1)] - + expected_geo_grid_points_reversed = [(-179.3, 89.3), (178.4, 79.3)] with self.subTest('Get two valid geo grid points from coordinates'): actual_geo_grid_points = get_two_valid_geo_grid_points( self.lat_arr, self.lon_arr, lat_fill, lon_fill, row_size, col_size ) - for actual, expected in zip( + for actual_geo_grid_point, expected_geo_grid_point in zip( actual_geo_grid_points.values(), expected_geo_grid_points ): - self.assertEqual(actual[0], expected[0]) - self.assertEqual(actual[1], expected[1]) + self.assertEqual(actual_geo_grid_point, expected_geo_grid_point) + + with self.subTest('Get two valid geo grid points from reversed coordinates'): + actual_geo_grid_points = get_two_valid_geo_grid_points( + self.lat_arr_reversed, + self.lon_arr_reversed, + lat_fill, + lon_fill, + row_size, + col_size, + ) + for actual_geo_grid_point, expected_geo_grid_point in zip( + actual_geo_grid_points.values(), expected_geo_grid_points_reversed + ): + self.assertEqual(actual_geo_grid_point, expected_geo_grid_point) + - prefetch_dataset.close() +# get_dimension_scale_from_dimvalues( +# dim_values: ndarray, dim_indices: ndarray, dim_size: float +# ) -> ndarray: