Skip to content

Commit

Permalink
Add dataset and data variable wrappers for common operations
Browse files Browse the repository at this point in the history
- Add dataset.py and variable.py which stores wrappers
- Update .gitignore
- Update axis.py to bounds.py to explicitly express intent
- Update readthedocs.yml
- Update setup.py with requirements list
- Update package imports in meta.yaml and api.rst
- Add `get_bounds_for_all_coords()` method
  • Loading branch information
tomvothecoder committed Aug 4, 2021
1 parent 6b811fa commit c6ab44d
Show file tree
Hide file tree
Showing 16 changed files with 926 additions and 129 deletions.
4 changes: 2 additions & 2 deletions CONTRIBUTING.rst
Original file line number Diff line number Diff line change
Expand Up @@ -183,14 +183,14 @@ Local Development
plugins: anyio-2.2.0, cov-2.11.1
collected 3 items

tests/test_utils.py ..
tests/test_dataset.py ..
tests/test_xcdat.py .

---------- coverage: platform darwin, python 3.8.8-final-0 -----------
Name Stmts Miss Cover
---------------------------------------
xcdat/__init__.py 3 0 100%
xcdat/utils.py 18 0 100%
xcdat/dataset.py 18 0 100%
xcdat/xcdat.py 0 0 100%
---------------------------------------
TOTAL 21 0 100%
Expand Down
3 changes: 1 addition & 2 deletions conda-env/dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,10 @@ dependencies:
- python=3.8.8
- pip=21.0.1
- typing_extensions=3.7.4 # Required to make use of Python >=3.8 backported types
- cartopy=0.18.0
- matplotlib=3.3.4
- netcdf4=1.5.6
- xarray=0.17.0
- cf_xarray=0.6.0
- dask=2021.7.0
# Additional
# ==================
- bump2version==1.0.1
Expand Down
4 changes: 2 additions & 2 deletions conda-env/readthedocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ dependencies:
- python=3.8.8
- pip=21.0.1
- typing_extensions=3.7.4 # Required to make use of Python >=3.8 backported types
- cartopy=0.18.0
- matplotlib=3.3.4
- netcdf4=1.5.6
- xarray=0.17.0
- dask=2021.7.0
- cf_xarray=0.6.0
# Documentation
# ==================
- sphinx=3.5.1
Expand Down
11 changes: 5 additions & 6 deletions conda-recipe/meta.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,18 @@ requirements:
run:
- python
- typing_extensions
- cartopy
- matplotlib
- netcdf4
- xarray
- typing_extensions
- dask
- cf_xarray

test:
imports:
- xcdat
- xcdat.logs
- xcdat.coord
- xcdat.utils
- xcdat.bounds
- xcdat.dataset
- xcdat.logger
- xcdat.variable

about:
home: https://github.com/tomvothecoder/xcdat
Expand Down
7 changes: 4 additions & 3 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ API Reference
.. autosummary::
:toctree: generated/

xcdat.coord
xcdat.log
xcdat.utils
xcdat.bounds
xcdat.dataset
xcdat.logger
xcdat.variable
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
history = history_file.read()

# https://packaging.python.org/discussions/install-requires-vs-requirements/#install-requires
requirements: List[str] = []
requirements: List[str] = ["xarray", "pandas", "numpy"]

setup_requirements = [
"pytest-runner",
Expand Down
168 changes: 168 additions & 0 deletions tests/fixtures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
"""This module stores reusable test fixtures."""
from datetime import datetime

import numpy as np
import xarray as xr

# If the fixture is an xarray object, make sure to use .copy() to create a
# shallow copy of the object. Otherwise, you might run into unintentional
# side-effects caused by reference assignment.
# https://xarray.pydata.org/en/stable/generated/xarray.DataArray.copy.html

# Dataset coordinates
time = xr.DataArray(
data=[
datetime(2000, 1, 1),
datetime(2000, 2, 1),
datetime(2000, 3, 1),
datetime(2000, 4, 1),
datetime(2000, 5, 1),
datetime(2000, 6, 1),
datetime(2000, 7, 1),
datetime(2000, 8, 1),
datetime(2000, 9, 1),
datetime(2000, 10, 1),
datetime(2000, 11, 1),
datetime(2000, 12, 1),
],
dims=["time"],
)
time_non_cf_compliant = xr.DataArray(
data=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
dims=["time"],
attrs={"units": "months since 2000-01-01"},
)

lat = xr.DataArray(
data=np.array([-90, -88.75, 88.75, 90]),
dims=["lat"],
attrs={"units": "degrees_north", "axis": "Y"},
)
lon = xr.DataArray(
data=np.array([0, 1.875, 356.25, 358.125]),
dims=["lon"],
attrs={"units": "degrees_east", "axis": "X"},
)

# Dataset data variables (bounds)
time_bnds = xr.DataArray(
name="time_bnds",
data=[
[datetime(1999, 12, 16, 12), datetime(2000, 1, 16, 12)],
[datetime(2000, 1, 16, 12), datetime(2000, 2, 15, 12)],
[datetime(2000, 2, 15, 12), datetime(2000, 3, 16, 12)],
[datetime(2000, 3, 16, 12), datetime(2000, 4, 16)],
[datetime(2000, 4, 16), datetime(2000, 5, 16, 12)],
[datetime(2000, 5, 16, 12), datetime(2000, 6, 16)],
[datetime(2000, 6, 16), datetime(2000, 7, 16, 12)],
[datetime(2000, 7, 16, 12), datetime(2000, 8, 16, 12)],
[datetime(2000, 8, 16, 12), datetime(2000, 9, 16)],
[datetime(2000, 9, 16), datetime(2000, 10, 16, 12)],
[datetime(2000, 10, 16, 12), datetime(2000, 11, 16)],
[datetime(2000, 11, 16), datetime(2000, 12, 16)],
],
coords={"time": time},
dims=["time", "bnds"],
attrs={"is_generated": "True"},
)

time_bnds_non_cf_compliant = xr.DataArray(
name="time_bnds",
data=[
[datetime(1999, 12, 16, 12), datetime(2000, 1, 16, 12)],
[datetime(2000, 1, 16, 12), datetime(2000, 2, 15, 12)],
[datetime(2000, 2, 15, 12), datetime(2000, 3, 16, 12)],
[datetime(2000, 3, 16, 12), datetime(2000, 4, 16)],
[datetime(2000, 4, 16), datetime(2000, 5, 16, 12)],
[datetime(2000, 5, 16, 12), datetime(2000, 6, 16)],
[datetime(2000, 6, 16), datetime(2000, 7, 16, 12)],
[datetime(2000, 7, 16, 12), datetime(2000, 8, 16, 12)],
[datetime(2000, 8, 16, 12), datetime(2000, 9, 16)],
[datetime(2000, 9, 16), datetime(2000, 10, 16, 12)],
[datetime(2000, 10, 16, 12), datetime(2000, 11, 16)],
[datetime(2000, 11, 16), datetime(2000, 12, 16)],
],
coords={"time": time.data},
dims=["time", "bnds"],
attrs={"is_generated": "True"},
)
lat_bnds = xr.DataArray(
name="lat_bnds",
data=np.array([[-90, -89.375], [-89.375, 0.0], [0.0, 89.375], [89.375, 90]]),
coords={"lat": lat.data},
dims=["lat", "bnds"],
attrs={"units": "degrees_north", "axis": "Y", "is_generated": "True"},
)
lon_bnds = xr.DataArray(
name="lon_bnds",
data=np.array(
[
[-0.9375, 0.9375],
[0.9375, 179.0625],
[179.0625, 357.1875],
[357.1875, 359.0625],
]
),
coords={"lon": lon.data},
dims=["lon", "bnds"],
attrs={"units": "degrees_east", "axis": "X", "is_generated": "True"},
)

# Dataset data variables (variables)
ts = xr.DataArray(
name="ts",
data=np.ones((12, 4, 4)),
coords={"time": time, "lat": lat, "lon": lon},
dims=["time", "lat", "lon"],
)
ts_non_cf_compliant = xr.DataArray(
name="ts",
data=np.ones((12, 4, 4)),
coords={"time": time_non_cf_compliant, "lat": lat, "lon": lon},
dims=["time", "lat", "lon"],
)


def generate_dataset(cf_compliant=True, has_bounds: bool = True) -> xr.Dataset:
"""Generates a dataset using coordinate and data variable fixtures.
NOTE: Using ``.assign()`` to add data variables to an existing dataset will
remove attributes from existing coordinates. The workaround is to update a
data_vars dict then create the dataset. https://github.com/pydata/xarray/issues/2245
Parameters
----------
cf_compliant : bool, optional
CF compliant time units, by default True
has_bounds : bool, optional
Include bounds for coordinates, by default True
Returns
-------
xr.Dataset
Test dataset.
"""
data_vars = {}
coords = {
"lat": lat.copy(),
"lon": lon.copy(),
}

if cf_compliant:
coords.update({"time": time.copy()})
data_vars.update({"ts": ts.copy()})
else:
coords.update({"time": time_non_cf_compliant.copy()})
data_vars.update({"ts": ts_non_cf_compliant.copy()})

if has_bounds:
data_vars.update(
{
"time_bnds": time_bnds.copy(),
"lat_bnds": lat_bnds.copy(),
"lon_bnds": lon_bnds.copy(),
}
)

ds = xr.Dataset(data_vars=data_vars, coords=coords)
return ds
56 changes: 44 additions & 12 deletions tests/test_coord.py → tests/test_bounds.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
import pytest
import xarray as xr

from xcdat.coord import CoordAccessor
from tests.fixtures import generate_dataset
from xcdat.bounds import DataArrayBoundsAccessor, DatasetBoundsAccessor


class TestCoordAccessor:
class TestDatasetBoundsAccessor:
@pytest.fixture(autouse=True)
def setup(self):
# Coordinate information
Expand Down Expand Up @@ -64,14 +65,14 @@ def setup(self):
self.ds = xr.Dataset(coords={"lat": lat, "lon": lon})

def test__init__(self):
obj = CoordAccessor(self.ds)
obj = DatasetBoundsAccessor(self.ds)
assert obj._dataset.identical(self.ds)

def test_decorator_call(self):
assert self.ds.coord._dataset.identical(self.ds)
assert self.ds.bounds._dataset.identical(self.ds)

def test_get_bounds_when_bounds_exist_in_dataset(self):
obj = CoordAccessor(self.ds)
obj = DatasetBoundsAccessor(self.ds)
obj._dataset = obj._dataset.assign(
lat_bnds=self.lat_bnds,
lon_bnds=self.lon_bnds,
Expand All @@ -87,7 +88,7 @@ def test_get_bounds_when_bounds_exist_in_dataset(self):

def test_get_bounds_when_bounds_do_not_exist_in_dataset(self):
# Check bounds generated if bounds do not exist.
obj = CoordAccessor(self.ds)
obj = DatasetBoundsAccessor(self.ds)

lat_bnds = obj.get_bounds("lat")
assert lat_bnds is not None
Expand All @@ -105,7 +106,7 @@ def test_get_bounds_when_bounds_do_not_exist_in_dataset(self):
obj.get_bounds("lat", allow_generating=False)

def test_get_bounds_raises_error_with_incorrect_axis_argument(self):
obj = CoordAccessor(self.ds)
obj = DatasetBoundsAccessor(self.ds)

with pytest.raises(ValueError):
obj.get_bounds("incorrect_axis_argument") # type: ignore
Expand All @@ -115,7 +116,7 @@ def test__get_bounds_does_not_drop_attrs_of_existing_coords_when_generating_boun
):
ds = self.ds.copy()

lat_bnds = ds.coord.get_bounds("lat", allow_generating=True)
lat_bnds = ds.bounds.get_bounds("lat", allow_generating=True)
assert lat_bnds.identical(self.lat_bnds)

ds = ds.drop("lat_bnds")
Expand All @@ -135,7 +136,7 @@ def test__generate_bounds_raises_errors_for_data_dim_and_length(self):
attrs={"units": "degrees_east", "axis": "X"},
)
ds = xr.Dataset(coords={"lat": lat, "lon": lon})
obj = CoordAccessor(ds)
obj = DatasetBoundsAccessor(ds)

# If coords dimensions does not equal 1.
with pytest.raises(ValueError):
Expand All @@ -145,7 +146,7 @@ def test__generate_bounds_raises_errors_for_data_dim_and_length(self):
obj._generate_bounds("lon")

def test__generate_bounds_returns_bounds(self):
obj = CoordAccessor(self.ds)
obj = DatasetBoundsAccessor(self.ds)

lat_bnds = obj._generate_bounds("lat")
assert lat_bnds.equals(self.lat_bnds)
Expand All @@ -156,7 +157,7 @@ def test__generate_bounds_returns_bounds(self):
assert obj._dataset.lon_bnds.is_generated

def test__get_coord(self):
obj = CoordAccessor(self.ds)
obj = DatasetBoundsAccessor(self.ds)

# Check lat axis coordinates exist
lat = obj._get_coord("lat")
Expand All @@ -167,8 +168,39 @@ def test__get_coord(self):
assert lon is not None

def test__get_coord_raises_error_if_coord_does_not_exist(self):
obj = CoordAccessor(self.ds)
obj = DatasetBoundsAccessor(self.ds)

with pytest.raises(KeyError):
obj._dataset = obj._dataset.drop_vars("lat")
obj._get_coord("lat")


class TestDataArrayBoundsAccessor:
@pytest.fixture(autouse=True)
def setUp(self):
self.ds = generate_dataset(has_bounds=True)
self.ds.lat.attrs["bounds"] = "lat_bnds"
self.ds.lon.attrs["bounds"] = "lon_bnds"
self.ds.time.attrs["bounds"] = "time_bnds"

def test_decorator_call(self):
expected = self.ds.ts
result = self.ds["ts"].bounds._dataarray
assert result.identical(expected)

def test__init__(self):
expected = self.ds.ts
result = DataArrayBoundsAccessor(self.ds["ts"])
assert result._dataarray.identical(expected)

assert result.lat is None
assert result.lon is None
assert result.time is None

def test__copy_from_parent_copies_bounds(self):
result = DataArrayBoundsAccessor(self.ds["ts"])
result._copy_from_dataset(self.ds)

assert self.ds.lat_bnds.identical(result.lat)
assert self.ds.lon_bnds.identical(result.lon)
assert self.ds.time_bnds.identical(result.time)
Loading

0 comments on commit c6ab44d

Please sign in to comment.