Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release 0.2.0 #7

Merged
merged 18 commits into from
Oct 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 2 additions & 10 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: ["3.10", "3.11", "3.12"]
python-version: ["3.10", "3.11", "3.12", "3.13"]

steps:
- uses: actions/checkout@v4
Expand All @@ -29,17 +29,9 @@ jobs:
with:
python-version: ${{ matrix.python-version }}

- name: API Key File
shell: bash
- name: Test
env:
CDS_API_KEY: ${{secrets.CDS_API_KEY}}
run: |
echo "url: https://cds.climate.copernicus.eu/api/v2" >> .cdsapirc
echo key: $CDS_API_KEY >> .cdsapirc
cat .cdsapirc
mv .cdsapirc ~/

- name: Test
run: |
pip install nox
pip install --upgrade setuptools
Expand Down
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,21 @@
[![Documentation Status](https://readthedocs.org/projects/bmi_era5/badge/?version=latest)](https://bmi-era5.readthedocs.io/en/latest/?badge=latest)
[![MIT license](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/gantian127/bmi_era5/blob/master/LICENSE.txt)

**Please be aware that the CDS platform was upgraded in September 2024, which has resulted in the current bmi_era5 being incompatible with the latest version of the CDS platform.
A new release of bmi_era5 is expected later this year.**
**Please note: Starting with release v0.2.0, the New CDS platform is now supported.**

bmi_era5 package is an implementation of the Basic Model Interface ([BMI](https://bmi-spec.readthedocs.io/en/latest/))
for the [ERA5](https://confluence.ecmwf.int/display/CKB/ERA5) dataset.
This package uses the [CDS API](https://cds.climate.copernicus.eu/api-how-to) to download the ERA5 dataset and wraps the dataset with BMI for data control and query
(currently support 3 dimensional ERA5 dataset).
This package uses the [CDS API](https://cds.climate.copernicus.eu/how-to-api) to download
the ERA5 dataset and wraps the dataset with BMI for data control and query.
It currently supports 3-dimensional ERA5
datasets defined with dimensions such as valid_time (or date), latitude, and longitude.

This package is not implemented for people to use but is the key element to convert the ERA5 dataset into
a data component ([pymt_era5](https://pymt-era5.readthedocs.io/)) for
the [PyMT](https://pymt.readthedocs.io/en/latest/?badge=latest) modeling framework developed
by Community Surface Dynamics Modeling System ([CSDMS](https://csdms.colorado.edu/wiki/Main_Page)).

If you have any suggestion to improve the current function, please create a github issue
If you have any suggestion to improve the current function, please create a GitHub issue
[here](https://github.com/gantian127/bmi_era5/issues).


Expand Down Expand Up @@ -77,7 +78,7 @@ c.retrieve(
dataset = xarray.open_dataset("download.nc")

# select 2 meter temperature on 2021-01-01 at 00:00
air_temp = dataset.t2m.isel(time=0)
air_temp = dataset.t2m.isel(valid_time=0)

# plot data
air_temp.plot(figsize=(9, 5))
Expand Down
7 changes: 4 additions & 3 deletions docs/source/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
[bmi_era5 package][bmi_era5-github] is an implementation of
the [Basic Model Interface (BMI)][bmi-docs] for the [ERA5][ERA5] dataset.
This package uses the [CDS API][cds-api] to download the ERA5 dataset and wraps the
dataset with BMI for data control and query (currently support 3 dimensional ERA5 dataset).
dataset with BMI for data control and query. It currently supports 3-dimensional ERA5
datasets defined with dimensions such as valid_time (or date), latitude, and longitude.

This package is not implemented for people to use but is the key element to convert the ERA5 dataset into
a data component ([pymt_era5][pymt_era5]) for the [PyMT][pymt-docs]
Expand Down Expand Up @@ -80,7 +81,7 @@ c.retrieve(
dataset = xarray.open_dataset("download.nc")

# select 2 metre temperature on 2021-01-01 at 00:00
air_temp = dataset.t2m.isel(time=0)
air_temp = dataset.t2m.isel(valid_time=0)

# plot data
air_temp.plot(figsize=(9, 5))
Expand Down Expand Up @@ -179,7 +180,7 @@ data_comp.finalize()
[bmi-docs]: https://bmi.readthedocs.io
[csdms]: https://csdms.colorado.edu
[pymt-docs]: https://pymt.readthedocs.io
[cds-api]: https://cds.climate.copernicus.eu/api-how-to
[cds-api]: https://cds.climate.copernicus.eu/how-to-api
[bmi_era5-github]: https://github.com/gantian127/bmi_era5/
[ERA5]: https://confluence.ecmwf.int/display/CKB/ERA5
[bmi_era5-notebook]: https://github.com/gantian127/bmi_era5/blob/master/notebooks/bmi_era5.ipynb
Expand Down
8 changes: 4 additions & 4 deletions notebooks/bmi_era5.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"bmi_era5 package is an implementation of the Basic Model Interface ([BMI](https://bmi.readthedocs.io/en/latest/)) for the [ERA5](https://confluence.ecmwf.int/display/CKB/ERA5) dataset. This package uses the [CDS API](https://cds.climate.copernicus.eu/api-how-to) to download the ERA5 dataset and wraps the dataset with BMI for data control and query (currently support 3 dimensional ERA5 dataset). This package is not implemented for people to use but is the key element to convert the ERA5 dataset into a data component for the [PyMT](https://pymt.readthedocs.io/en/latest/?badge=latest) modeling framework developed by Community Surface Dynamics Modeling System ([CSDMS](https://csdms.colorado.edu/wiki/Main_Page)).\n",
"bmi_era5 package is an implementation of the Basic Model Interface ([BMI](https://bmi.readthedocs.io/en/latest/)) for the [ERA5](https://confluence.ecmwf.int/display/CKB/ERA5) dataset. This package uses the [CDS API](https://cds.climate.copernicus.eu/how-to-api) to download the ERA5 dataset and wraps the dataset with BMI for data control and query (currently support 3-dimensional ERA5 dataset). This package is not implemented for people to use but is the key element to convert the ERA5 dataset into a data component for the [PyMT](https://pymt.readthedocs.io/en/latest/?badge=latest) modeling framework developed by Community Surface Dynamics Modeling System ([CSDMS](https://csdms.colorado.edu/wiki/Main_Page)).\n",
"\n",
" \n",
"To install bmi_era5 package, please follow the instructions [here](https://github.com/gantian127/bmi_era5/#install-package)."
Expand All @@ -59,7 +59,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"You can uncomment the code below to use install_cds( ) to install the CDS API Key file. This file is required for data download. So please make sure you have already created an account at the [CDS registration page](https://cds.climate.copernicus.eu/#!/home) and obtained your CDS API Key. For more details please check [here](https://cds.climate.copernicus.eu/api-how-to)."
"You can uncomment the code below to use install_cds( ) to install the CDS API Key file. This file is required for data download. So please make sure you have already created an account at the [CDS registration page](https://cds.climate.copernicus.eu/#!/home) and obtained your CDS API Key. For more details please check [here](https://cds.climate.copernicus.eu/how-to-api)."
]
},
{
Expand Down Expand Up @@ -152,7 +152,7 @@
"dataset = xarray.open_dataset(\"download.nc\")\n",
"\n",
"# select 2 metre temperature on 2021-01-01 at 00:00\n",
"air_temp = dataset.t2m.isel(time=0)"
"air_temp = dataset.t2m.isel(valid_time=0)"
]
},
{
Expand Down Expand Up @@ -392,7 +392,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.7"
"version": "3.13.0"
}
},
"nbformat": 4,
Expand Down
Binary file modified notebooks/download.nc
Binary file not shown.
7 changes: 1 addition & 6 deletions notebooks/installation.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

def install_cds(config_path=""):
# create configuration file
url = "https://cds.climate.copernicus.eu/api/v2"
url = "https://cds.climate.copernicus.eu/api"
key = input("Enter Your CDS API Key: ")
config_content = f"url: {url} \nkey: {key}"

Expand All @@ -16,8 +16,3 @@ def install_cds(config_path=""):
config_file.write(config_content)

print("CDS API Key file is created.")

# # install cdsapi
# install = subprocess.run(["pip", "install", "cdsapi"], check=True)
# if install.returncode == 0:
# print('cdsapi package is successfully installed.')
9 changes: 9 additions & 0 deletions noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ def test(session: nox.Session) -> None:
"""Run the tests."""
session.install(".[testing]")

# create API key file for ERA5 (need GitHub secret)
url = "https://cds.climate.copernicus.eu/api"
key = os.environ.get("CDS_API_KEY")
config_content = f"url: {url} \nkey: {key}"
home_dir = os.path.expanduser("~")
config_path = os.path.join(home_dir, ".cdsapirc")
with open(config_path, "w") as config_file:
config_file.write(config_content)

args = ["--cov", PROJECT, "-vvv"] + session.posargs

if "CI" in os.environ:
Expand Down
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ classifiers = [
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: Implementation :: CPython",
"Topic :: Scientific/Engineering :: Hydrology",
"Topic :: Scientific/Engineering :: Physics",
Expand All @@ -33,7 +34,8 @@ dependencies = [
"pyyaml",
"requests",
"xarray",
"cdsapi",
"cdsapi >= 0.7.2",
"cftime",
]
dynamic = [
"version",
Expand Down
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
bmipy
cdsapi>=0.7.2
cftime
netcdf4
numpy
pyyaml
Expand Down
2 changes: 1 addition & 1 deletion src/bmi_era5/_version.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from __future__ import annotations

__version__ = "0.1.4"
__version__ = "0.2.0"
10 changes: 2 additions & 8 deletions src/bmi_era5/bmi.py
Original file line number Diff line number Diff line change
Expand Up @@ -462,14 +462,8 @@ def get_value_ptr(self, name: str) -> numpy.ndarray:
"""
# return a reference of all the value at current time step.
# mainly for input data. not useful for scalar value
add_offset = self._dataset[self._var_name_mapping[name]].add_offset
scale_factor = self._dataset[self._var_name_mapping[name]].scale_factor

return (
self._dataset[self._var_name_mapping[name]].values[self._time_index]
* scale_factor
+ add_offset
)

return self._dataset[self._var_name_mapping[name]].values[self._time_index]

def get_var_grid(self, name: str) -> int:
"""Get grid identifier for the given variable.
Expand Down
74 changes: 52 additions & 22 deletions src/bmi_era5/utils.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
from __future__ import annotations

import os.path
from datetime import datetime

import cdsapi
import cftime
import numpy as np
import xarray as xr


Expand Down Expand Up @@ -81,17 +84,44 @@ def get_time_info(self):

# time values are float in BMI time function
if self._data:
time_info = {
"start_time": float(self._data.time.values[0]),
"time_step": 0.0
if len(self._data.time.values) == 1
else float(self._data.time.values[1] - self._data.time.values[0]),
"end_time": float(self._data.time.values[-1]),
"total_steps": len(self._data.time.values),
"time_units": self._data.time.units,
"calendar": self._data.time.calendar,
"time_value": self._data.time.values.astype("float"),
}
if "valid_time" in self._data.keys():
time_info = {
"start_time": float(self._data.valid_time.values[0]),
"time_step": 0.0
if len(self._data.valid_time.values) == 1
else float(
self._data.valid_time.values[1]
- self._data.valid_time.values[0]
),
"end_time": float(self._data.valid_time.values[-1]),
"total_steps": len(self._data.valid_time.values),
"time_units": self._data.valid_time.units,
"calendar": self._data.valid_time.calendar,
"time_value": self._data.valid_time.values.astype("float"),
}
elif "date" in self._data.keys():
# convert date time to CF convention values
date_objs = [
datetime.strptime(str(date_value), "%Y%m%d")
for date_value in self._data.date.values
]
time_units = "seconds since 1970-01-01"
calendar = "proleptic_gregorian"
cf_dates = cftime.date2num(
date_objs, units=time_units, calendar=calendar
)

time_info = {
"start_time": float(cf_dates[0]),
"time_step": 0.0
if len(cf_dates) == 1
else float(cf_dates[1] - cf_dates[0]),
"end_time": float(cf_dates[-1]),
"total_steps": len(cf_dates),
"time_units": time_units,
"calendar": calendar,
"time_value": np.array(cf_dates, dtype=float),
}

return time_info

Expand All @@ -101,16 +131,16 @@ def get_var_info(self):
if self._data:
for var_name in self._data.data_vars:
var = self._data.data_vars[var_name]

var_info[var.long_name] = {
"var_name": var_name,
"dtype": type(var.scale_factor).__name__
if "scale_factor" in var.attrs.keys()
else str(var.dtype),
"itemsize": var.values.itemsize,
"nbytes": var.values[0].nbytes, # current time step nbytes
"units": var.units,
"location": "node",
}
if var.ndim >= 3:
var_info[var.long_name] = {
"var_name": var_name,
"dtype": type(var.scale_factor).__name__
if "scale_factor" in var.attrs.keys()
else str(var.dtype),
"itemsize": var.values.itemsize,
"nbytes": var.values[0].nbytes, # current time step nbytes
"units": var.units,
"location": "node",
}

return var_info
60 changes: 50 additions & 10 deletions tests/util_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,23 @@
)
]

parameters2 = [
(
"reanalysis-era5-single-levels-monthly-means",
"monthly_mean.nc",
{
"product_type": ["monthly_averaged_reanalysis"],
"variable": ["2m_dewpoint_temperature"],
"year": ["2022"],
"month": ["01", "02", "03", "04"],
"time": ["00:00"],
"data_format": "netcdf",
"download_format": "unarchived",
"area": [39, -106, 36, -103],
},
)
]


@pytest.mark.parametrize("name, file, era5_req", parameters)
def test_get_data(tmpdir, name, file, era5_req):
Expand Down Expand Up @@ -62,21 +79,22 @@ def test_get_var_info(tmpdir, name, file, era5_req):

era5.get_data(name, era5_req, path)
var_info_2 = era5.get_var_info()
assert len(var_info_2) == 2

assert "Total precipitation" in var_info_2.keys()
assert "2 metre temperature" in var_info_2.keys()

var = var_info_2["Total precipitation"]
assert var["var_name"] == "tp"
assert var["dtype"] == "float64"
assert var["itemsize"] == 2
assert var["nbytes"] == 1218
assert var["dtype"] == "float32"
assert var["itemsize"] == 4
assert var["nbytes"] == 2436
assert var["units"] == "m"
assert var["location"] == "node"


@pytest.mark.parametrize("name, file, era5_req", parameters)
def test_get_time_info(tmpdir, name, file, era5_req):
def test_get_time_info_valid_time(tmpdir, name, file, era5_req):
"""Test when time variable is valid_time"""
path = os.path.join(tmpdir, file)

era5 = Era5Data()
Expand All @@ -87,9 +105,31 @@ def test_get_time_info(tmpdir, name, file, era5_req):
era5.get_data(name, era5_req, path)
time_info_2 = era5.get_time_info()

assert time_info_2["start_time"] == 1060680
assert time_info_2["end_time"] == 1060682
assert time_info_2["time_step"] == 1
assert time_info_2["start_time"] == 1609459200.0
assert time_info_2["end_time"] == 1609466400.0
assert time_info_2["time_step"] == 3600.0
assert time_info_2["total_steps"] == 3
assert time_info_2["time_units"] == "hours since 1900-01-01 00:00:00.0"
assert time_info_2["calendar"] == "gregorian"
assert time_info_2["time_units"] == "seconds since 1970-01-01"
assert time_info_2["calendar"] == "proleptic_gregorian"


@pytest.mark.parametrize("name, file, era5_req", parameters2)
def test_get_time_info_date(tmpdir, name, file, era5_req):
"""Test when time variable is date"""

path = os.path.join(tmpdir, file)

era5 = Era5Data()
time_info_1 = era5.get_time_info()

assert time_info_1 == {}

era5.get_data(name, era5_req, path)
time_info_2 = era5.get_time_info()

assert time_info_2["start_time"] == 1640995200.0
assert time_info_2["end_time"] == 1648771200.0
assert time_info_2["time_step"] == 2678400.0
assert time_info_2["total_steps"] == 4
assert time_info_2["time_units"] == "seconds since 1970-01-01"
assert time_info_2["calendar"] == "proleptic_gregorian"
Loading