From deb279651b133599cc473af17fc29a8f1b222568 Mon Sep 17 00:00:00 2001 From: Josie Lyon <82548520+lyonthefrog@users.noreply.github.com> Date: Thu, 14 Mar 2024 18:25:38 -0500 Subject: [PATCH] =?UTF-8?q?DAS-1177:=20Add=20functions=20and=20CF=20overri?= =?UTF-8?q?des=20to=20create=20artificial=20bounds=20=E2=80=A6=20(#5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * DAS-1177: Add functions and CF overrides to create artificial bounds for collections with edge-alignment. * DAS-1177 - Update the code to improve variable names, replace some code with simpler/best practice code, and make a lot of minor formatting updates. * DAS-1177 - Remove leading/trailing whitespaces, update method of extracting names from dimension path. * DAS-1177 - Add and update unit tests - add_bounds not complete yet. * DAS-1177 - Add test data file, update data file README, small data file adjustments. * DAS-1177 - Create end-to-end test, finish unit tests, edit test data files. * DAS-1177 - Minor comment updates. * DAS-1177 - Minor updates to test_dimension_utilities.py * DAS-1177 - Make minor style updates, verify expected index ranges in end-to-end test, add tests to increase coverage in the unit test. * DAS-1177 - Added missing tests and updated comment for index range in the end-to-end test * DAS-1177 - remove duplicate tests --- CHANGELOG.md | 16 ++ docker/service_version.txt | 2 +- hoss/dimension_utilities.py | 147 +++++++++++++++- hoss/hoss_config.json | 27 +++ tests/data/ATL16_prefetch.dmr | 225 +++++++++++++++++++++++++ tests/data/ATL16_prefetch.nc4 | Bin 0 -> 49996 bytes tests/data/ATL16_prefetch_bnds.dmr | 220 ++++++++++++++++++++++++ tests/data/ATL16_prefetch_group.dmr | 219 ++++++++++++++++++++++++ tests/data/ATL16_prefetch_group.nc4 | Bin 0 -> 72202 bytes tests/data/ATL16_variables.nc4 | Bin 0 -> 81041 bytes tests/data/README.md | 155 ++++++++++------- tests/test_adapter.py | 115 ++++++++++++- tests/unit/test_dimension_utilities.py | 181 +++++++++++++++++++- 13 files changed, 1235 insertions(+), 72 deletions(-) create mode 100644 tests/data/ATL16_prefetch.dmr create mode 100644 tests/data/ATL16_prefetch.nc4 create mode 100644 tests/data/ATL16_prefetch_bnds.dmr create mode 100644 tests/data/ATL16_prefetch_group.dmr create mode 100644 tests/data/ATL16_prefetch_group.nc4 create mode 100644 tests/data/ATL16_variables.nc4 diff --git a/CHANGELOG.md b/CHANGELOG.md index 037358d..d2fe83c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,19 @@ +## v1.0.2 +### 2024-2-26 + +This version of HOSS correctly handles edge-aligned geographic collections by +adding the attribute `cell_alignment` with the value `edge` to `hoss_config.json` +for edge-aligned collections (namely, ATL16), and by adding functions that +create pseudo bounds for edge-aligned collections to make HOSS use the +`dimension_utilities.py` function, `get_dimension_indices_from_bounds`. The +pseudo bounds are only used internally and are not returned in the HOSS subset. + +This change also includes an addition of a CF override that addresses an +issue with the ATL16 metadata for the variables `/spolar_asr_obs_grid` and +`/spolar_lorate_blowing_snow_freq` where their `grid_mapping` attribute points +to north polar variables instead of south polar variables. This CF Override +can be removed if/when the metadata is corrected. + ## v1.0.1 ### 2023-12-19 diff --git a/docker/service_version.txt b/docker/service_version.txt index 7dea76e..6d7de6e 100644 --- a/docker/service_version.txt +++ b/docker/service_version.txt @@ -1 +1 @@ -1.0.1 +1.0.2 diff --git a/hoss/dimension_utilities.py b/hoss/dimension_utilities.py index 2db6397..ab6b10a 100644 --- a/hoss/dimension_utilities.py +++ b/hoss/dimension_utilities.py @@ -12,6 +12,7 @@ from logging import Logger from typing import Dict, Set, Tuple +from pathlib import PurePosixPath from netCDF4 import Dataset from numpy.ma.core import MaskedArray import numpy as np @@ -19,7 +20,7 @@ from harmony.message import Message from harmony.message_utility import rgetattr from harmony.util import Config -from varinfo import VarInfoFromDmr +from varinfo import VarInfoFromDmr, VariableFromDmr from hoss.bbox_utilities import flatten_list from hoss.exceptions import InvalidNamedDimension, InvalidRequestedRange @@ -75,8 +76,148 @@ def prefetch_dimension_variables(opendap_url: str, varinfo: VarInfoFromDmr, logger.info('Variables being retrieved in prefetch request: ' f'{format_variable_set_string(required_dimensions)}') - return get_opendap_nc4(opendap_url, required_dimensions, output_dir, - logger, access_token, config) + + 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 add_bounds_variables(dimensions_nc4: str, + required_dimensions: Set[str], + varinfo: VarInfoFromDmr, + logger: Logger) -> None: + """ Augment a NetCDF4 file with artificial bounds variables for each + dimension variable that has been identified by the earthdata-varinfo + configuration file to have an edge-aligned attribute" + + For each dimension variable: + (1) Check if the variable needs a bounds variable. + (2) If so, create a bounds array from within the `write_bounds` + function. + (3) Then write the bounds variable to the NetCDF4 URL. + + """ + with Dataset(dimensions_nc4, 'r+') as prefetch_dataset: + for dimension_name in required_dimensions: + dimension_variable = varinfo.get_variable(dimension_name) + if needs_bounds(dimension_variable): + write_bounds(prefetch_dataset, dimension_variable) + + logger.info('Artificial bounds added for dimension variable: ' + f'{dimension_name}') + + +def needs_bounds(dimension: VariableFromDmr) -> bool: + """ Check if a dimension variable needs a bounds variable. + This will be the case when dimension cells are edge-aligned + and bounds for that dimension do not already exist. + + """ + return ( + dimension.attributes.get('cell_alignment') == 'edge' + and dimension.references.get('bounds') is None + ) + + +def get_bounds_array(prefetch_dataset: Dataset, + dimension_path: str) -> np.ndarray: + """ Create an array containing the minimum and maximum bounds + for each pixel in a given dimension. + + The minimum and maximum values are determined under the assumption + that the dimension data is monotonically increasing and contiguous. + So for every bounds but the last, the bounds are simply extracted + from the dimension dataset. + + The final bounds must be calculated with the assumption that + the last data cell is edge-aligned and thus has a value the does + not account for the cell length. So, the final bound is determined + by taking the median of all the resolutions in the dataset to obtain + a resolution that can be added to the final data value. + + Ex: Input dataset with resolution of 3 degrees: [ ... , 81, 84, 87] + + Minimum | Maximum + <...> <...> + 81 84 + 84 87 + 87 ? -> 87 + median resolution -> 87 + 3 -> 90 + + """ + # Access the dimension variable's data using the variable's full path. + dimension_array = prefetch_dataset[dimension_path][:] + + median_resolution = np.median(np.diff(dimension_array)) + + # This array is the transpose of what is required, just for easier assignment + # of values (indices are [row, column]) during the bounds calculations: + cell_bounds = np.zeros(shape=(2, dimension_array.size), dtype=dimension_array.dtype) + + # Minimum values are equal to the dimension pixel values (for lower left pixel alignment): + cell_bounds[0] = dimension_array[:] + + # Maximum values are the next dimension pixel values (for lower left pixel alignment), + # so these values almost mirror the minimum values but start at the second pixel + # instead of the first. Here we calculate each bound except for the very last one. + cell_bounds[1][:-1] = dimension_array[1:] + + # Last maximum value is the last pixel value (minimum) plus the median resolution: + cell_bounds[1][-1] = dimension_array[-1] + median_resolution + + # Return transpose of array to get correct shape: + return cell_bounds.T + + +def write_bounds(prefetch_dataset: Dataset, + dimension_variable: VariableFromDmr) -> None: + """ Write the input bounds array to a given dimension dataset. + + First a new dimension is created for the new bounds variable + to allow the variable to be two-dimensional. + + Then the new bounds variable is created using two dimensions: + (1) the existing dimension of the dimension dataset, and + (2) the new bounds variable dimension. + + """ + bounds_array = get_bounds_array(prefetch_dataset, + dimension_variable.full_name_path) + + # Create the second bounds dimension. + dimension_name = str(PurePosixPath(dimension_variable.full_name_path).name) + dimension_group = str(PurePosixPath(dimension_variable.full_name_path).parent) + bounds_name = dimension_name + '_bnds' + bounds_full_path_name = dimension_variable.full_name_path + '_bnds' + bounds_dimension_name = dimension_name + 'v' + + if dimension_group == '/': + # The root group must be explicitly referenced here. + bounds_dim = prefetch_dataset.createDimension(bounds_dimension_name, 2) + else: + bounds_dim = prefetch_dataset[dimension_group].createDimension(bounds_dimension_name, 2) + + # Dimension variables only have one dimension - themselves. + variable_dimension = prefetch_dataset[dimension_variable.full_name_path].dimensions[0] + + bounds_data_type = str(dimension_variable.data_type) + bounds = prefetch_dataset.createVariable(bounds_full_path_name, + bounds_data_type, + (variable_dimension, + bounds_dim,)) + + # Write data to the new variable in the prefetch dataset. + bounds[:] = bounds_array[:] + + # Update varinfo attributes and references. + prefetch_dataset[dimension_variable.full_name_path].setncatts({'bounds': bounds_name}) + dimension_variable.references['bounds'] = {bounds_name, } + dimension_variable.attributes['bounds'] = bounds_name def is_dimension_ascending(dimension: MaskedArray) -> bool: diff --git a/hoss/hoss_config.json b/hoss/hoss_config.json index d960a8f..cefe364 100644 --- a/hoss/hoss_config.json +++ b/hoss/hoss_config.json @@ -245,6 +245,20 @@ ], "_Description": "Ensure variables in /Soil_Moisture_Retrieval_Data_Polar_PM group point to correct coordinate variables." }, + { + "Applicability": { + "Mission": "ICESat2", + "ShortNamePath": "ATL16", + "Variable_Pattern": ".*_grid_(lat|lon)" + }, + "Attributes": [ + { + "Name": "cell_alignment", + "Value": "edge" + } + ], + "_Description": "ATL16 has edge-aligned grid cells." + }, { "Applicability": { "Mission": "ICESat2", @@ -357,6 +371,19 @@ } ], "_Description": "Ensure the latitude and longitude dimension variables know their associated grid_mapping variable." + }, + { + "Applicability": { + "Mission": "ICESat2", + "ShortNamePath": "ATL16", + "Variable_Pattern": "/spolar_(asr_obs_grid|lorate_blowing_snow_freq)" + }, + "Attributes": [ + { + "Name": "grid_mapping", + "Value": "crs_latlon: spolar_grid_lat crs_latlon: spolar_grid_lon" + } + ] } ], "CF_Supplements": [ diff --git a/tests/data/ATL16_prefetch.dmr b/tests/data/ATL16_prefetch.dmr new file mode 100644 index 0000000..7ddecd8 --- /dev/null +++ b/tests/data/ATL16_prefetch.dmr @@ -0,0 +1,225 @@ + + + + + + + + + + + + north polar grid longitude + + + longitude + + + degrees_east + + + L3B ATM ATBD, Section 2.0, Section 5.0, Table 5. + + + -180. + + + 180. + + + referenceInformation + + + The north polar grid longitude one-dimensional array parameter, with the longitudes applicable to each north polar grid cell. ATL09 ATM histogram top longitude (longitude ()) derived from the ATM range window geolocation. Based on WGS84 Earth-centered, Earth-fixed terrestrial reference system and geodetic data. Direction: East=+ values. Range of longitude values: -180.0 to +180.0 degrees, with a step size of polar_grid_lon_scale. Grid array organization: npolar_grid_lon (i), where i=longitude index, as per ATBD Section 2.0. Given weekly polar_grid_lon_scale = 3.0 degrees, the weekly array dimension is: npolar_grid_lon (120), type: double precision. Comprises the x-grid axis for north polar gridded parameters. Reference: npolar_grid_lon (1) = -180.0 degrees, at the upper left corner of a north polar gridded parameter cell location (i,j) = (1,1). + + + X + + + + + + south polar grid latitude + + + latitude + + + degrees_north + + + L3B ATM ATBD, Section 2.0, Section 5.0, Table 5. + + + -90. + + + -60. + + + referenceInformation + + + The south polar grid latitude one-dimensional array parameter, with the latitudes applicable to each south polar grid cell. ATL09 ATM histogram top latitude (latitude ()) derived from the ATM range window geolocation. Based on WGS84 Earth-centered, Earth-fixed terrestrial reference system and geodetic data. Direction: South=- values. Range of latitude values: -90.0 up to -60.0 degrees, with a step size of polar_grid_lat_scale. Grid array organization: spolar_grid_lat (j), where j=latitude index, as per ATBD Section 2.0. Given weekly polar_grid_lat_scale = 1.0 degrees, the weekly array dimension is: spolar_grid_lat (30), type: double precision. Comprises the y-grid axis for south polar gridded parameters. Reference: spolar_grid_lat (1) = -90.0 degrees, at the lower left corner of a south polar gridded parameter cell location (i,j) = (1,1). + + + Y + + + + + + global grid longitude + + + longitude + + + degrees_east + + + L3B ATM ATBD, Section 2.0, Section 5.0, Table 5. + + + -180. + + + 180. + + + referenceInformation + + + The global grid longitude one-dimensional array parameter, with the longitudes applicable to each global grid cell. The ATL09 ATM histogram top longitude (longitude()) derived from the ATM range window geolocation. Based on WGS84 Earth-centered, Earth-fixed terrestrial reference system and geodetic data. Direction: East=+ values. Range of longitude values: -180.0 to +180.0 degrees, with a step size of global_grid_lon_scale. Grid array organization: global_grid_lon (i), where i=longitude index, as per ATBD Section 2.0. Given weekly global_grid_lon_scale = 3.0 degrees, array dimension: global_grid_lon (120), type: double precision. Comprises the x-grid axis for global gridded parameters. Reference: global_grid_lon (1) = -180.0 degrees, at the lower left corner of a global gridded parameter cell location (i,j) = (1,1). + + + X + + + + + + global grid latitude + + + latitude + + + degrees_north + + + L3B ATM ATBD, Section 2.0, Section 5.0, Table 5. + + + -90. + + + 90. + + + referenceInformation + + + The global grid latitude one-dimensional array parameter, with the latitudes applicable to each global grid cell. ATL09 ATM histogram top latitude (latitude ()) derived from the ATM range window geolocation. Based on WGS84 Earth-centered, Earth-fixed terrestrial reference system and geodetic data. Direction: North=+ values. Range of latitude values: -90.0 to +90.0 degrees, with a step size of global_grid_lat_scale. Grid array organization: global_grid_lat (j), where j=latitude index, as per ATBD Section 2.0. Given weekly global_grid_lat_scale = 3.0 degrees, array dimension: global_grid_lat (60), type: double precision. Comprises the y-grid axis for global gridded parameters. Reference: global_grid_lat (1) = -90.0 degrees, at the lower left corner of a global gridded parameter cell location (i,j) = (1,1). + + + Y + + + + + + south polar grid longitude + + + longitude + + + degrees_east + + + L3B ATM ATBD, Section 2.0, Section 5.0, Table 5. + + + -180. + + + 180. + + + referenceInformation + + + The south polar grid longitude one-dimensional array parameter, with the longitudes applicable to each south polar grid cell. ATL09 ATM histogram top longitude (longitude ()) derived from the ATM range window geolocation. Based on WGS84 Earth-centered, Earth-fixed terrestrial reference system and geodetic data. Direction: East=+ values. Range of longitude values: -180.0 to +180.0 degrees, with a step size of polar_grid_lon_scale. Grid array organization: spolar_grid_lon (i), where i=longitude index, as per ATBD Section 2.0. Given weekly polar_grid_lon_scale = 3.0 degrees, the weekly array dimension is: spolar_grid_lon (120), type: double precision. Comprises the x-grid axis for south polar gridded parameters. Reference: spolar_grid_lon (1) = -180.0 degrees, at the lower left corner of a south polar gridded parameter cell location (i,j) = (1,1). + + + X + + + + + + north polar grid latitude + + + latitude + + + degrees_north + + + L3B ATM ATBD, Section 2.0, Section 5.0, Table 5. + + + 60. + + + 90. + + + referenceInformation + + + The north polar grid latitude one-dimensional array parameter, with the latitudes applicable to each north polar grid cell. ATL09 ATM histogram top latitude (latitude ()) derived from the ATM range window geolocation. Based on WGS84 Earth-centered, Earth-fixed terrestrial reference system and geodetic data. Direction: North=+ values. Range of latitude values: +90.0 down to +60.0 degrees, with a step size of polar_grid_lat_scale. Grid array organization: npolar_grid_lat (j), where j=latitude index, as per ATBD Section 2.0. Given weekly polar_grid_lat_scale = 1.0 degrees, the weekly array dimension is: npolar_grid_lat (30), type: double precision. Comprises the y-grid axis for north polar gridded parameters. Reference: npolar_grid_lat (1) = +90.0 degrees, at the upper left corner of a north polar gridded parameter cell location (i,j) = (1,1). + + + Y + + + + + + + + global apparent surface reflectance observation grid + + + + 1 + + + + L3B ATM ATBD, Section 3.9, Table 4., Section 5.0, Table 5. + + + + modelResult + + + + The global apparent surface reflectance (ASR) observation count two-dimensional gridded parameter. The number of observations used to compute the average global apparent surface reflectance (ASR). Only surface signal detected ASR 25 Hz (high-rate profile) observations (apparent_surf_reflec () > 0.0) in each grid cell are used to compute the cell global ASR (global_asr (i,j)). Grid array organization: global_asr_obs_grid (i,j), where i=longitude index, j=latitude index, as per ATBD Section 2.0. Given the weekly global_grid_lon_scale = 3.0 degrees and the global_grid_lat_scale = 3.0 degrees, the weekly global gridded array dimension is: global_asr_obs_grid (120,60), type: floating point . Reference: global_asr_obs_grid (1,1) = (-180.0 degrees longitude, -90.0 degrees latitude), representing the lower left corner of the gridded parameter cell in the global projection array. The global observation counts are applicable to the atmosphere gridded parameter: global_asr (i,j). + + + + global_grid_lat global_grid_lon + + + + crs_latlon: global_grid_lat crs_latlon: global_grid_lon + + + + + ATL16 + + \ No newline at end of file diff --git a/tests/data/ATL16_prefetch.nc4 b/tests/data/ATL16_prefetch.nc4 new file mode 100644 index 0000000000000000000000000000000000000000..28b10546014f557107519fa9d6ced2a7bb1c19b8 GIT binary patch literal 49996 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 + + + + + + + + + + + + north polar grid longitude + + + longitude + + + degrees_east + + + L3B ATM ATBD, Section 2.0, Section 5.0, Table 5. + + + -180. + + + 180. + + + referenceInformation + + + The north polar grid longitude one-dimensional array parameter, with the longitudes applicable to each north polar grid cell. ATL09 ATM histogram top longitude (longitude ()) derived from the ATM range window geolocation. Based on WGS84 Earth-centered, Earth-fixed terrestrial reference system and geodetic data. Direction: East=+ values. Range of longitude values: -180.0 to +180.0 degrees, with a step size of polar_grid_lon_scale. Grid array organization: npolar_grid_lon (i), where i=longitude index, as per ATBD Section 2.0. Given weekly polar_grid_lon_scale = 3.0 degrees, the weekly array dimension is: npolar_grid_lon (120), type: double precision. Comprises the x-grid axis for north polar gridded parameters. Reference: npolar_grid_lon (1) = -180.0 degrees, at the upper left corner of a north polar gridded parameter cell location (i,j) = (1,1). + + + X + + + + + + south polar grid latitude + + + latitude + + + degrees_north + + + L3B ATM ATBD, Section 2.0, Section 5.0, Table 5. + + + -90. + + + -60. + + + referenceInformation + + + The south polar grid latitude one-dimensional array parameter, with the latitudes applicable to each south polar grid cell. ATL09 ATM histogram top latitude (latitude ()) derived from the ATM range window geolocation. Based on WGS84 Earth-centered, Earth-fixed terrestrial reference system and geodetic data. Direction: South=- values. Range of latitude values: -90.0 up to -60.0 degrees, with a step size of polar_grid_lat_scale. Grid array organization: spolar_grid_lat (j), where j=latitude index, as per ATBD Section 2.0. Given weekly polar_grid_lat_scale = 1.0 degrees, the weekly array dimension is: spolar_grid_lat (30), type: double precision. Comprises the y-grid axis for south polar gridded parameters. Reference: spolar_grid_lat (1) = -90.0 degrees, at the lower left corner of a south polar gridded parameter cell location (i,j) = (1,1). + + + Y + + + + + + global grid longitude + + + longitude + + + degrees_east + + + L3B ATM ATBD, Section 2.0, Section 5.0, Table 5. + + + -180. + + + 180. + + + referenceInformation + + + The global grid longitude one-dimensional array parameter, with the longitudes applicable to each global grid cell. The ATL09 ATM histogram top longitude (longitude()) derived from the ATM range window geolocation. Based on WGS84 Earth-centered, Earth-fixed terrestrial reference system and geodetic data. Direction: East=+ values. Range of longitude values: -180.0 to +180.0 degrees, with a step size of global_grid_lon_scale. Grid array organization: global_grid_lon (i), where i=longitude index, as per ATBD Section 2.0. Given weekly global_grid_lon_scale = 3.0 degrees, array dimension: global_grid_lon (120), type: double precision. Comprises the x-grid axis for global gridded parameters. Reference: global_grid_lon (1) = -180.0 degrees, at the lower left corner of a global gridded parameter cell location (i,j) = (1,1). + + + X + + + + + + global grid latitude + + + latitude + + + degrees_north + + + L3B ATM ATBD, Section 2.0, Section 5.0, Table 5. + + + -90. + + + 90. + + + referenceInformation + + + The global grid latitude one-dimensional array parameter, with the latitudes applicable to each global grid cell. ATL09 ATM histogram top latitude (latitude ()) derived from the ATM range window geolocation. Based on WGS84 Earth-centered, Earth-fixed terrestrial reference system and geodetic data. Direction: North=+ values. Range of latitude values: -90.0 to +90.0 degrees, with a step size of global_grid_lat_scale. Grid array organization: global_grid_lat (j), where j=latitude index, as per ATBD Section 2.0. Given weekly global_grid_lat_scale = 3.0 degrees, array dimension: global_grid_lat (60), type: double precision. Comprises the y-grid axis for global gridded parameters. Reference: global_grid_lat (1) = -90.0 degrees, at the lower left corner of a global gridded parameter cell location (i,j) = (1,1). + + + Y + + + + + + south polar grid longitude + + + longitude + + + degrees_east + + + L3B ATM ATBD, Section 2.0, Section 5.0, Table 5. + + + -180. + + + 180. + + + referenceInformation + + + The south polar grid longitude one-dimensional array parameter, with the longitudes applicable to each south polar grid cell. ATL09 ATM histogram top longitude (longitude ()) derived from the ATM range window geolocation. Based on WGS84 Earth-centered, Earth-fixed terrestrial reference system and geodetic data. Direction: East=+ values. Range of longitude values: -180.0 to +180.0 degrees, with a step size of polar_grid_lon_scale. Grid array organization: spolar_grid_lon (i), where i=longitude index, as per ATBD Section 2.0. Given weekly polar_grid_lon_scale = 3.0 degrees, the weekly array dimension is: spolar_grid_lon (120), type: double precision. Comprises the x-grid axis for south polar gridded parameters. Reference: spolar_grid_lon (1) = -180.0 degrees, at the lower left corner of a south polar gridded parameter cell location (i,j) = (1,1). + + + X + + + + + + north polar grid latitude + + + latitude + + + degrees_north + + + L3B ATM ATBD, Section 2.0, Section 5.0, Table 5. + + + 60. + + + 90. + + + referenceInformation + + + The north polar grid latitude one-dimensional array parameter, with the latitudes applicable to each north polar grid cell. ATL09 ATM histogram top latitude (latitude ()) derived from the ATM range window geolocation. Based on WGS84 Earth-centered, Earth-fixed terrestrial reference system and geodetic data. Direction: North=+ values. Range of latitude values: +90.0 down to +60.0 degrees, with a step size of polar_grid_lat_scale. Grid array organization: npolar_grid_lat (j), where j=latitude index, as per ATBD Section 2.0. Given weekly polar_grid_lat_scale = 1.0 degrees, the weekly array dimension is: npolar_grid_lat (30), type: double precision. Comprises the y-grid axis for north polar gridded parameters. Reference: npolar_grid_lat (1) = +90.0 degrees, at the upper left corner of a north polar gridded parameter cell location (i,j) = (1,1). + + + Y + + + + + + + + + bnds_variable + + + edge + + + + + + bnds_variable + + + + + + edge + + + + + + + ATL16 + + \ No newline at end of file diff --git a/tests/data/ATL16_prefetch_group.dmr b/tests/data/ATL16_prefetch_group.dmr new file mode 100644 index 0000000..c200956 --- /dev/null +++ b/tests/data/ATL16_prefetch_group.dmr @@ -0,0 +1,219 @@ + + + + + + + + zelda the good dog + + + zelda dog + + + Unknown. + + + 10 inches + + + 9 pounds + + + 917-years-old + + + Zelda the Good Dog hails from unknown regions of the world. While her age was once estimated to be around 7-years-old by a veterinary professional, it is believed that this estimate was made under intimidation by the dog herself in order to conceal the true nature of her being: an ancient omniscient sage. + + + + + + + + + + + + + north polar grid longitude + + + longitude + + + degrees_east + + + L3B ATM ATBD, Section 2.0, Section 5.0, Table 5. + + + -180. + + + 180. + + + referenceInformation + + + The north polar grid longitude one-dimensional array parameter, with the longitudes applicable to each north polar grid cell. ATL09 ATM histogram top longitude (longitude ()) derived from the ATM range window geolocation. Based on WGS84 Earth-centered, Earth-fixed terrestrial reference system and geodetic data. Direction: East=+ values. Range of longitude values: -180.0 to +180.0 degrees, with a step size of polar_grid_lon_scale. Grid array organization: npolar_grid_lon (i), where i=longitude index, as per ATBD Section 2.0. Given weekly polar_grid_lon_scale = 3.0 degrees, the weekly array dimension is: npolar_grid_lon (120), type: double precision. Comprises the x-grid axis for north polar gridded parameters. Reference: npolar_grid_lon (1) = -180.0 degrees, at the upper left corner of a north polar gridded parameter cell location (i,j) = (1,1). + + + X + + + + + + south polar grid latitude + + + latitude + + + degrees_north + + + L3B ATM ATBD, Section 2.0, Section 5.0, Table 5. + + + -90. + + + -60. + + + referenceInformation + + + The south polar grid latitude one-dimensional array parameter, with the latitudes applicable to each south polar grid cell. ATL09 ATM histogram top latitude (latitude ()) derived from the ATM range window geolocation. Based on WGS84 Earth-centered, Earth-fixed terrestrial reference system and geodetic data. Direction: South=- values. Range of latitude values: -90.0 up to -60.0 degrees, with a step size of polar_grid_lat_scale. Grid array organization: spolar_grid_lat (j), where j=latitude index, as per ATBD Section 2.0. Given weekly polar_grid_lat_scale = 1.0 degrees, the weekly array dimension is: spolar_grid_lat (30), type: double precision. Comprises the y-grid axis for south polar gridded parameters. Reference: spolar_grid_lat (1) = -90.0 degrees, at the lower left corner of a south polar gridded parameter cell location (i,j) = (1,1). + + + Y + + + + + + global grid longitude + + + longitude + + + degrees_east + + + L3B ATM ATBD, Section 2.0, Section 5.0, Table 5. + + + -180. + + + 180. + + + referenceInformation + + + The global grid longitude one-dimensional array parameter, with the longitudes applicable to each global grid cell. The ATL09 ATM histogram top longitude (longitude()) derived from the ATM range window geolocation. Based on WGS84 Earth-centered, Earth-fixed terrestrial reference system and geodetic data. Direction: East=+ values. Range of longitude values: -180.0 to +180.0 degrees, with a step size of global_grid_lon_scale. Grid array organization: global_grid_lon (i), where i=longitude index, as per ATBD Section 2.0. Given weekly global_grid_lon_scale = 3.0 degrees, array dimension: global_grid_lon (120), type: double precision. Comprises the x-grid axis for global gridded parameters. Reference: global_grid_lon (1) = -180.0 degrees, at the lower left corner of a global gridded parameter cell location (i,j) = (1,1). + + + X + + + + + + global grid latitude + + + latitude + + + degrees_north + + + L3B ATM ATBD, Section 2.0, Section 5.0, Table 5. + + + -90. + + + 90. + + + referenceInformation + + + The global grid latitude one-dimensional array parameter, with the latitudes applicable to each global grid cell. ATL09 ATM histogram top latitude (latitude ()) derived from the ATM range window geolocation. Based on WGS84 Earth-centered, Earth-fixed terrestrial reference system and geodetic data. Direction: North=+ values. Range of latitude values: -90.0 to +90.0 degrees, with a step size of global_grid_lat_scale. Grid array organization: global_grid_lat (j), where j=latitude index, as per ATBD Section 2.0. Given weekly global_grid_lat_scale = 3.0 degrees, array dimension: global_grid_lat (60), type: double precision. Comprises the y-grid axis for global gridded parameters. Reference: global_grid_lat (1) = -90.0 degrees, at the lower left corner of a global gridded parameter cell location (i,j) = (1,1). + + + Y + + + + + + south polar grid longitude + + + longitude + + + degrees_east + + + L3B ATM ATBD, Section 2.0, Section 5.0, Table 5. + + + -180. + + + 180. + + + referenceInformation + + + The south polar grid longitude one-dimensional array parameter, with the longitudes applicable to each south polar grid cell. ATL09 ATM histogram top longitude (longitude ()) derived from the ATM range window geolocation. Based on WGS84 Earth-centered, Earth-fixed terrestrial reference system and geodetic data. Direction: East=+ values. Range of longitude values: -180.0 to +180.0 degrees, with a step size of polar_grid_lon_scale. Grid array organization: spolar_grid_lon (i), where i=longitude index, as per ATBD Section 2.0. Given weekly polar_grid_lon_scale = 3.0 degrees, the weekly array dimension is: spolar_grid_lon (120), type: double precision. Comprises the x-grid axis for south polar gridded parameters. Reference: spolar_grid_lon (1) = -180.0 degrees, at the lower left corner of a south polar gridded parameter cell location (i,j) = (1,1). + + + X + + + + + + north polar grid latitude + + + latitude + + + degrees_north + + + L3B ATM ATBD, Section 2.0, Section 5.0, Table 5. + + + 60. + + + 90. + + + referenceInformation + + + The north polar grid latitude one-dimensional array parameter, with the latitudes applicable to each north polar grid cell. ATL09 ATM histogram top latitude (latitude ()) derived from the ATM range window geolocation. Based on WGS84 Earth-centered, Earth-fixed terrestrial reference system and geodetic data. Direction: North=+ values. Range of latitude values: +90.0 down to +60.0 degrees, with a step size of polar_grid_lat_scale. Grid array organization: npolar_grid_lat (j), where j=latitude index, as per ATBD Section 2.0. Given weekly polar_grid_lat_scale = 1.0 degrees, the weekly array dimension is: npolar_grid_lat (30), type: double precision. Comprises the y-grid axis for north polar gridded parameters. Reference: npolar_grid_lat (1) = +90.0 degrees, at the upper left corner of a north polar gridded parameter cell location (i,j) = (1,1). + + + Y + + + + ATL16 + + \ No newline at end of file diff --git a/tests/data/ATL16_prefetch_group.nc4 b/tests/data/ATL16_prefetch_group.nc4 new file mode 100644 index 0000000000000000000000000000000000000000..83772d89d96b87da90d1a8fc562ce3a7dcc38639 GIT binary patch literal 72202 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 literal 0 HcmV?d00001 diff --git a/tests/data/ATL16_variables.nc4 b/tests/data/ATL16_variables.nc4 new file mode 100644 index 0000000000000000000000000000000000000000..e6eee6e18374055ada157ee4251301c1faac6918 GIT binary patch literal 81041 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

lLxMs&bhw& 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 literal 0 HcmV?d00001 diff --git a/tests/data/README.md b/tests/data/README.md index db2610e..d06a301 100644 --- a/tests/data/README.md +++ b/tests/data/README.md @@ -1,65 +1,94 @@ # Listing of test files -* ABoVE_TVPRM_bbox.nc4 - This is an example output from a bounding box request - to the ABoVE TVPRM collection, where -160 ≤ longitude (degrees east) ≤ -145, - 68 ≤ latitude (degrees north) ≤ 70. Note, ABoVE TVPRM has 8784 time slices. - To minimise the size of the stored artefact in Git, this example output only - contains the first 10 time slices. ABoVE TVPRM has a projected grid that uses - an Albers Conical Equal Area CRS. -* ABoVE_TVPRM_example.dmr - An example `.dmr` file for the ABoVE TVPRM - collection, as obtained from OPeNDAP. -* ABoVE_TVPRM_prefetch.nc4 - An example dimension prefetch output from OPeNDAP - for the ABoVE TVPRM collection. This contains the `time`, `x` and `y` - variables. -* ATL03_example.dmr - An example `.dmr` file from the ICESat-2/ATL03 collection, - as obtained from OPeNDAP. ATL03 is a trajectory data set and should only be - used (currently) with the variable subsetting operations of the service. -* GPM_3IMERGHH_bounds.nc4 - An example output from a bounding box and temporal - subset request for a collection containing bounds variables. The bounding box - is -30 ≤ longitude (degrees east) ≤ -15, 45 ≤ latitude (degrees north) ≤ 60. -* GPM_3IMERGHH_example.dmr - An example `.dmr` file for the GPM/3IMERGHH - collection, as obtained from OPeNDAP. GPM/3IMERGHH has a half-hourly time - dimension to the grid. It also contains bounds variable references. -* GPM_3IMERGHH_prefetch.nc4 - An example dimension prefetch output from OPeNDAP - for the GPM/IMERGHH colleciton. This contains the `/Grid/time`, `/Grid/lat`, - and `/Grid/lon` dimension variables along with their associated bounds - variables. -* M2T1NXSLV_example.dmr` - An example `.dmr` file for a MERRA-2 collection. - Granules in this collection are geographically gridded and contain a time - dimension that has half-hour resolution. -* M2T1NXSLV_geo_temporal.nc4 - Example output for a MERRA-2 collection. This - output is for both a spatial (bounding box) and temporal subset. -* M2T1NXSLV_prefetch.nc4 - An example dimension prefetch output from OPeNDAP - for a MERRA-2 collection. This contains a longitude, latitude and temporal - dimension. -* M2T1NXSLV_temporal.nc4 - An example output for a MERRA-2 collection, with a - request to OPeNDAP for a temporal subset. -* f16_ssmis_20200102v7.nc - An input granule for the RSSMIF16D collection. The - variables in this collection a 3-dimensional, gridded with a latitude and - longitude dimension and a single element time dimension. RSSMIF16D also has - 0 ≤ longitude (degrees east) ≤ 360. -* f16_ssmis_filled.nc - The output from a spatial subset request when the - requested bounding box crosses the Prime Meridian (and therefore the edge of - the grid). -* f16_ssmis_geo.nc - The output from a spatial and variable subset request with - a bounding box. -* f16_ssmis_geo_desc.nc - The output from a spatial and variable subset request - with a bounding box input. The latitude dimension is also descending in this - example, unlike the native ascending ordering. -* f16_ssmis_geo_no_vars.nc - The results of a spatial subset only with a - bounding box. -* f16_ssmis_lat_lon.nc - The output for a prefetch request that retrieves only - the latitude and longitude variables for the RSSMIF16D collection. This - sample predates the temporal subsetting work, and so does not also include - the temporal dimension variable. -* f16_ssmis_lat_lon_desc.nc - The output for a prefetch request that retrieves - only the latitude and longitude variables for the RSSMIF16D collection. This - differs from the previous sample file, as the latitude dimension is descending - in this file. -* f16_ssmis_unfilled.nc - The sample output for a request to OPeNDAP when the - longitude range crosses the edge of the bounding box. In this case, the data - retrieved from OPeNDAP will be a band in latitude, but cover the full - longitudinal range. HOSS will later fill the required region of that band - before returning the result to the end-user. -* rssmif16d_example.dmr - An example `.dmr` file as retrieved from OPeNDAP for - the RSSMIF16D collection. +* ABoVE_TVPRM_bbox.nc4 + - This is an example output from a bounding box request to the ABoVE + TVPRM collection, where -160 ≤ longitude (degrees east) ≤ -145, 68 ≤ latitude + (degrees north) ≤ 70. Note, ABoVE TVPRM has 8784 time slices. To minimise the + size of the stored artefact in Git, this example output only contains the first + 10 time slices. ABoVE TVPRM has a projected grid that uses an Albers Conical + Equal Area CRS. +* ABoVE_TVPRM_example.dmr + - An example `.dmr` file for the ABoVE TVPRM collection, as obtained from OPeNDAP +* ABoVE_TVPRM_prefetch.nc4 + - An example dimension prefetch output from OPeNDAP for the ABoVE TVPRM collection. + This contains the `time`, `x` and `y` variables +* ATL03_example.dmr + - An example `.dmr` file from the ICESat-2/ATL03 collection, as obtained from + OPeNDAP. ATL03 is a trajectory data set and should only be used (currently) + with the variable subsetting operations of the service +* GPM_3IMERGHH_bounds.nc4 + - An example output from a bounding box and temporal subset request for + a collection containing bounds variables. The bounding box is -30 ≤ longitude + (degrees east) ≤ -15, 45 ≤ latitude (degrees north) ≤ 60 +* GPM_3IMERGHH_example.dmr + - An example `.dmr` file for the GPM/3IMERGHH collection, as obtained from + OPeNDAP. GPM/3IMERGHH has a half-hourly time dimension to the grid. It also + contains bounds variable references +* GPM_3IMERGHH_prefetch.nc4 + - An example dimension prefetch output from OPeNDAP for the GPM/IMERGHH + collection. This contains the `/Grid/time`, `/Grid/lat`, and `/Grid/lon` + dimension variables along with their associated bounds variables +* M2T1NXSLV_example.dmr + - An example `.dmr` file for a MERRA-2 collection. Granules in this collection + are geographically gridded and contain a time dimension that has half-hour + resolution +* M2T1NXSLV_geo_temporal.nc4 + - Example output for a MERRA-2 collection. This output is for both a spatial + (bounding box) and temporal subset +* M2T1NXSLV_prefetch.nc4 + - An example dimension prefetch output from OPeNDAP for a MERRA-2 collection. + This contains a longitude, latitude and temporal dimension +* M2T1NXSLV_temporal.nc4 + - An example output for a MERRA-2 collection, with a request to OPeNDAP for + a temporal subset +* f16_ssmis_20200102v7.nc + - An input granule for the RSSMIF16D collection. The variables in this + collection a 3-dimensional, gridded with a latitude and longitude dimension + and a single element time dimension. RSSMIF16D also has 0 ≤ longitude + (degrees east) ≤ 360 +* f16_ssmis_filled.nc + - The output from a spatial subset request when the requested bounding box + crosses the Prime Meridian (and therefore the edge of the grid) +* f16_ssmis_geo.nc + - The output from a spatial and variable subset request with a bounding box +* f16_ssmis_geo_desc.nc + - The output from a spatial and variable subset request with a bounding box + input. The latitude dimension is also descending in this example, unlike + the native ascending ordering +* f16_ssmis_geo_no_vars.nc + - The results of a spatial subset only with a bounding box +* f16_ssmis_lat_lon.nc + - The output for a prefetch request that retrieves only the latitude and + longitude variables for the RSSMIF16D collection. This sample predates + the temporal subsetting work, and so does not also include the temporal + dimension variable +* f16_ssmis_lat_lon_desc.nc + - The output for a prefetch request that retrieves only the latitude and + longitude variables for the RSSMIF16D collection. This differs from the + previous sample file, as the latitude dimension is descending in this file +* f16_ssmis_unfilled.nc + - The sample output for a request to OPeNDAP when the longitude range crosses + the edge of the bounding box. In this case, the data retrieved from OPeNDAP + will be a band in latitude, but cover the full longitudinal range. HOSS + will later fill the required region of that band before returning the result + to the end-user +* rssmif16d_example.dmr + - An example `.dmr` file as retrieved from OPeNDAP for the RSSMIF16D collection +* ATL16_prefetch.dmr + - An example `.dmr` file retrieved from OPeNDAP for the ATL16 collection, but whittled + down to only contain the six required dimension variables. +* ATL16_prefetch.nc4 + - A sample output file that contains the six required dimension variables and one + requested science variable in the ATL16 collection. +* ATL16_prefetch_group.dmr + - An example `.dmr` file that is identical to the `ATL16_prefetch.dmr` file + except for an additional fabricated nested group variable, whereas all the variables in the + ATL16 collection are in the root directory. +* ATL16_prefetch_group.nc4 + - A sample output file that is nearly identical to the `ATL16_prefetch.nc4` file except + for an additional fabricated nested group variable (the same one in the + ATL16_prefetch_group.dmr file). +* ATL16_prefetch_bnds.dmr + - An example `.dmr` file that is nearly identical to the `ATL16_prefetch.dmr` file + except for four additional fabricated variables that represented the four + possible cases of combining bounds variable existence and cell alignment. \ No newline at end of file diff --git a/tests/test_adapter.py b/tests/test_adapter.py index fed0da0..6ceff4c 100755 --- a/tests/test_adapter.py +++ b/tests/test_adapter.py @@ -32,6 +32,7 @@ def setUpClass(cls): cls.atl03_variable = '/gt1r/geophys_corr/geoid' cls.gpm_variable = '/Grid/precipitationCal' cls.rssmif16d_variable = '/wind_speed' + cls.atl16_variable = '/global_asr_obs_grid' cls.staging_location = 's3://example-bucket/' with open('tests/data/ATL03_example.dmr', 'r') as file_handler: @@ -46,6 +47,9 @@ def setUpClass(cls): with open('tests/data/GPM_3IMERGHH_example.dmr', 'r') as file_handler: cls.gpm_imerghh_dmr = file_handler.read() + with open('tests/data/ATL16_prefetch.dmr', 'r') as file_handler: + cls.atl16_dmr = file_handler.read() + def setUp(self): """ Have to mock mkdtemp, to know where to put mock .dmr content. """ self.tmp_dir = mkdtemp() @@ -84,7 +88,7 @@ def assert_expected_output_catalog(self, catalog: Catalog, self.assertListEqual(list(items[0].assets.keys()), ['data']) self.assertDictEqual( items[0].assets['data'].to_dict(), - {'href': expected_href, + {'href': expected_href, 'title': expected_title, 'type': 'application/x-netcdf4', 'roles': ['data']} @@ -2008,3 +2012,112 @@ def test_exception_handling(self, mock_stage, mock_download_subset, mock_stage.assert_not_called() mock_rmtree.assert_called_once_with(self.tmp_dir) + + @patch('hoss.dimension_utilities.get_fill_slice') + @patch('hoss.utilities.uuid4') + @patch('hoss.adapter.mkdtemp') + @patch('shutil.rmtree') + @patch('hoss.utilities.util_download') + @patch('hoss.adapter.stage') + def test_edge_aligned_no_bounds_end_to_end(self, mock_stage, + mock_util_download, + mock_rmtree, mock_mkdtemp, + mock_uuid, + mock_get_fill_slice): + """ Ensure a request for a collection that contains dimension variables + with edge-aligned grid cells is correctly processed regardless of + whether or not a bounds variable associated with that dimension + variable exists. + + """ + expected_output_basename = 'opendap_url_global_asr_obs_grid_subsetted.nc4' + expected_staged_url = f'{self.staging_location}{expected_output_basename}' + + mock_uuid.side_effect = [Mock(hex='uuid'), Mock(hex='uuid2')] + mock_mkdtemp.return_value = self.tmp_dir + mock_stage.return_value = expected_staged_url + + dmr_path = write_dmr(self.tmp_dir, self.atl16_dmr) + + dimensions_path = f'{self.tmp_dir}/dimensions.nc4' + copy('tests/data/ATL16_prefetch.nc4', dimensions_path) + + all_variables_path = f'{self.tmp_dir}/variables.nc4' + copy('tests/data/ATL16_variables.nc4', all_variables_path) + + mock_util_download.side_effect = [dmr_path, dimensions_path, + all_variables_path] + + message = Message({ + 'accessToken': 'fake-token', + 'callback': 'https://example.com/', + 'sources': [{ + 'collection': 'C1238589498-EEDTEST', + 'shortName': 'ATL16', + 'variables': [{'id': '', + 'name': self.atl16_variable, + 'fullPath': self.atl16_variable}]}], + 'stagingLocation': self.staging_location, + 'subset': {'bbox': [77, 71.25, 88, 74.75]}, + 'user': 'sride', + }) + + hoss = HossAdapter(message, config=config(False), catalog=self.input_stac) + _, output_catalog = hoss.invoke() + + # Ensure that there is a single item in the output catalog with the + # expected asset: + self.assert_expected_output_catalog(output_catalog, + expected_staged_url, + expected_output_basename) + + # Ensure the expected requests were made against OPeNDAP. + self.assertEqual(mock_util_download.call_count, 3) + mock_util_download.assert_has_calls([ + call(f'{self.granule_url}.dmr.xml', self.tmp_dir, hoss.logger, + access_token=message.accessToken, data=None, cfg=hoss.config), + call(f'{self.granule_url}.dap.nc4', self.tmp_dir, hoss.logger, + access_token=message.accessToken, data=ANY, cfg=hoss.config), + call(f'{self.granule_url}.dap.nc4', self.tmp_dir, hoss.logger, + access_token=message.accessToken, data=ANY, cfg=hoss.config), + ]) + + # Ensure the constraint expression for dimensions data included only + # dimension variables and their associated bounds variables. + dimensions_data = mock_util_download.call_args_list[1][1].get('data', {}) + self.assert_valid_request_data( + dimensions_data, + {'%2Fglobal_grid_lat', + '%2Fglobal_grid_lon'} + ) + + # Ensure the constraint expression contains all the required variables. + # The latitude and longitude index ranges here depend on whether + # the cells have centre-alignment or edge-alignment. + # Previously, the incorrect index ranges assuming centre-alignment: + # latitude [54:55] with values (72,75) + # longitude [86:89] with values (78,81,84,87) + # + # Now, the correct index ranges with edge-alignment: + # latitude: [53:54] for values (69,72). + # longitude:[85:89] for values (75,78,81,84,87) + # + index_range_data = mock_util_download.call_args_list[2][1].get('data', {}) + self.assert_valid_request_data( + index_range_data, + {'%2Fglobal_asr_obs_grid%5B53%3A54%5D%5B85%3A89%5D', + '%2Fglobal_grid_lat%5B53%3A54%5D', + '%2Fglobal_grid_lon%5B85%3A89%5D'} + ) + + # Ensure the output was staged with the expected file name + mock_stage.assert_called_once_with(f'{self.tmp_dir}/uuid2.nc4', + expected_output_basename, + 'application/x-netcdf4', + location=self.staging_location, + logger=hoss.logger) + + mock_rmtree.assert_called_once_with(self.tmp_dir) + + # Ensure no variables were filled + mock_get_fill_slice.assert_not_called() diff --git a/tests/unit/test_dimension_utilities.py b/tests/unit/test_dimension_utilities.py index 370fb74..e4fda16 100644 --- a/tests/unit/test_dimension_utilities.py +++ b/tests/unit/test_dimension_utilities.py @@ -7,6 +7,7 @@ from harmony.util import config from harmony.message import Message +from pathlib import PurePosixPath from netCDF4 import Dataset from numpy.ma import masked_array from numpy.testing import assert_array_equal @@ -22,7 +23,11 @@ get_requested_index_ranges, is_almost_in, is_dimension_ascending, is_index_subset, - prefetch_dimension_variables) + prefetch_dimension_variables, + add_bounds_variables, + needs_bounds, + get_bounds_array, + write_bounds) from hoss.exceptions import InvalidNamedDimension, InvalidRequestedRange @@ -45,6 +50,16 @@ def setUpClass(cls): cls.varinfo_with_bounds = VarInfoFromDmr( 'tests/data/GPM_3IMERGHH_example.dmr' ) + cls.bounds_array = np.array([ + [90.0, 89.0], [89.0, 88.0], [88.0, 87.0], [87.0, 86.0], + [86.0, 85.0], [85.0, 84.0], [84.0, 83.0], [83.0, 82.0], + [82.0, 81.0], [81.0, 80.0], [80.0, 79.0], [79.0, 78.0], + [78.0, 77.0], [77.0, 76.0], [76.0, 75.0], [75.0, 74.0], + [74.0, 73.0], [73.0, 72.0], [72.0, 71.0], [71.0, 70.0], + [70.0, 69.0], [69.0, 68.0], [68.0, 67.0], [67.0, 66.0], + [66.0, 65.0], [65.0, 64.0], [64.0, 63.0], [63.0, 62.0], + [62.0, 61.0], [61.0, 60.0] + ]) def setUp(self): """ Create fixtures that should be unique per test. """ @@ -225,8 +240,7 @@ def test_get_dimension_indices_from_indices(self): def test_add_index_range(self): """ Ensure the correct combinations of index ranges are added as - suffixes to the input variable based upon that variable's - dimensions. + suffixes to the input variable based upon that variable's dimensions. If a dimension range has the lower index > upper index, that indicates the bounding box crosses the edge of the grid. In this @@ -272,8 +286,10 @@ def test_get_fill_slice(self): slice(16, 200) ) + @patch('hoss.dimension_utilities.add_bounds_variables') @patch('hoss.dimension_utilities.get_opendap_nc4') - def test_prefetch_dimension_variables(self, mock_get_opendap_nc4): + def test_prefetch_dimension_variables(self, mock_get_opendap_nc4, + mock_add_bounds_variables): """ Ensure that when a list of required variables is specified, a request to OPeNDAP will be sent requesting only those that are grid-dimension variables (both spatial and temporal). @@ -304,6 +320,163 @@ def test_prefetch_dimension_variables(self, mock_get_opendap_nc4): output_dir, self.logger, access_token, self.config) + mock_add_bounds_variables.assert_called_once_with(prefetch_path, + required_dimensions, + self.varinfo, self.logger) + + @patch('hoss.dimension_utilities.needs_bounds') + @patch('hoss.dimension_utilities.write_bounds') + def test_add_bounds_variables(self, mock_write_bounds, mock_needs_bounds): + """ Ensure that `write_bounds` is called when it's needed, + and that it's not called when it's not needed. + + """ + prefetch_dataset_name = 'tests/data/ATL16_prefetch.nc4' + varinfo_prefetch = VarInfoFromDmr( + 'tests/data/ATL16_prefetch.dmr' + ) + required_dimensions = {'/npolar_grid_lat', '/npolar_grid_lon', + '/spolar_grid_lat', '/spolar_grid_lon', + '/global_grid_lat', '/global_grid_lon'} + + with self.subTest('Bounds need to be written'): + mock_needs_bounds.return_value = True + add_bounds_variables(prefetch_dataset_name, + required_dimensions, + varinfo_prefetch, + self.logger) + self.assertEqual(mock_write_bounds.call_count, 6) + + mock_needs_bounds.reset_mock() + mock_write_bounds.reset_mock() + + with self.subTest('Bounds should not be written'): + mock_needs_bounds.return_value = False + add_bounds_variables(prefetch_dataset_name, + required_dimensions, + varinfo_prefetch, + self.logger) + mock_write_bounds.assert_not_called() + + def test_needs_bounds(self): + """ Ensure that the correct boolean value is returned for four + different cases: + + 1) False - cell_alignment[edge] attribute exists and + bounds variable already exists. + 2) False - cell_alignment[edge] attribute does not exist and + bounds variable already exists. + 3) True - cell_alignment[edge] attribute exists and + bounds variable does not exist. + 4) False - cell_alignment[edge] attribute does not exist and + bounds variable does not exist. + + """ + varinfo_bounds = VarInfoFromDmr( + 'tests/data/ATL16_prefetch_bnds.dmr' + ) + + with self.subTest('Variable has cell alignment and bounds'): + self.assertFalse(needs_bounds(varinfo_bounds.get_variable( + '/variable_edge_has_bnds'))) + + with self.subTest('Variable has no cell alignment and has bounds'): + self.assertFalse(needs_bounds(varinfo_bounds.get_variable( + '/variable_no_edge_has_bnds'))) + + with self.subTest('Variable has cell alignment and no bounds'): + self.assertTrue(needs_bounds(varinfo_bounds.get_variable( + '/variable_edge_no_bnds'))) + + with self.subTest('Variable has no cell alignment and no bounds'): + self.assertFalse(needs_bounds(varinfo_bounds.get_variable( + '/variable_no_edge_no_bnds'))) + + def test_get_bounds_array(self): + """ Ensure that the expected bounds array is created given + the input dimension variable values. + + """ + prefetch_dataset = Dataset('tests/data/ATL16_prefetch.nc4', 'r') + dimension_path = '/npolar_grid_lat' + + expected_bounds_array = self.bounds_array + + assert_array_equal(get_bounds_array(prefetch_dataset, + dimension_path), + expected_bounds_array) + + def test_write_bounds(self): + """ Ensure that bounds data array is written to the dimension + dataset, both when the dimension variable is in the root group + and in a nested group. + + """ + varinfo_prefetch = VarInfoFromDmr('tests/data/ATL16_prefetch_group.dmr') + prefetch_dataset = Dataset('tests/data/ATL16_prefetch_group.nc4', 'r+') + + # Expected variable contents in file. + expected_bounds_data = self.bounds_array + + with self.subTest('Dimension variable is in the root group'): + root_variable_full_path = '/npolar_grid_lat' + root_varinfo_variable = varinfo_prefetch.get_variable( + root_variable_full_path) + root_variable_name = 'npolar_grid_lat' + root_bounds_name = root_variable_name + '_bnds' + + write_bounds(prefetch_dataset, root_varinfo_variable) + + # Check that bounds variable was written to the root group. + self.assertTrue(prefetch_dataset.variables[root_bounds_name]) + + resulting_bounds_root_data = prefetch_dataset.variables[ + root_bounds_name][:] + + assert_array_equal(resulting_bounds_root_data, + expected_bounds_data) + # Check that varinfo variable has 'bounds' attribute. + self.assertEqual(root_varinfo_variable.attributes['bounds'], + root_bounds_name) + # Check that NetCDF4 dimension variable has 'bounds' attribute. + self.assertEqual(prefetch_dataset.variables[ + root_variable_name].__dict__.get('bounds'), + root_bounds_name) + # Check that VariableFromDmr has 'bounds' reference in + # the references dictionary. + self.assertEqual(root_varinfo_variable.references['bounds'], + {root_bounds_name, }) + + with self.subTest('Dimension variable is in a nested group'): + nested_variable_full_path = '/group1/group2/zelda' + nested_varinfo_variable = varinfo_prefetch.get_variable( + nested_variable_full_path) + nested_variable_name = 'zelda' + nested_group_path = '/group1/group2' + nested_group = prefetch_dataset[nested_group_path] + nested_bounds_name = nested_variable_name + '_bnds' + + write_bounds(prefetch_dataset, nested_varinfo_variable) + + # Check that bounds variable exists in the nested group. + self.assertTrue(nested_group.variables[nested_bounds_name]) + + resulting_bounds_nested_data = nested_group.variables[ + nested_bounds_name][:] + assert_array_equal(resulting_bounds_nested_data, + expected_bounds_data) + # Check that varinfo variable has 'bounds' attribute. + self.assertEqual(nested_varinfo_variable.attributes['bounds'], + nested_bounds_name) + # Check that NetCDF4 dimension variable has 'bounds' attribute. + self.assertEqual(nested_group.variables[ + nested_variable_name].__dict__.get('bounds'), + nested_bounds_name) + # Check that VariableFromDmr 'has bounds' reference in + # the references dictionary. + self.assertEqual(nested_varinfo_variable.references['bounds'], + {nested_bounds_name, }) + @patch('hoss.dimension_utilities.get_opendap_nc4') def test_prefetch_dimensions_with_bounds(self, mock_get_opendap_nc4): """ Ensure that a variable which has dimensions with `bounds` metadata