Skip to content

Commit 42bbe7a

Browse files
authored
Merge pull request #3547 from divine7022/era5-cdsapi-tweaks
Enhance ERA5 Download Function: Direct NetCDF, Flexible Parameters, and Improved Docs
2 parents 4748cb7 + dae7402 commit 42bbe7a

8 files changed

Lines changed: 334 additions & 115 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ section for the next release.
5555
* Modules `PEcAn.allometry`, `PEcAn.assim.batch`, `PEcAn.data.mining`, `PEcAn.emulator`, `PEcAn.MA`, `PEcAn.photosynthesis`, `PEcAn.priors`, and `PEcAn.RTM`.
5656
- Renamed master branch to main
5757
- `PEcAn.all::pecan_version()` now reports commit hashes as well as version numbers for each installed package.
58+
- `download.ERA5_cds` now uses the R package ecmwfr (replacing python dependency of cdsapi via reticulate), enabling direct NetCDF downloads; and made flexible for both reanalysis and ensemble data product.
5859

5960
### Removed
6061

docker/depends/pecan_package_dependencies.csv

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
"dplyr","*","modules/uncertainty","Imports",FALSE
6565
"dplyr",">= 0.8.1","modules/data.atmosphere","Imports",FALSE
6666
"dplyr",">= 1.1.2","base/db","Imports",FALSE
67+
"ecmwfr",">= 2.0.0","modules/data.atmosphere","Suggests",FALSE
6768
"ellipse","*","modules/assim.batch","Imports",FALSE
6869
"exactextractr","*","modules/assim.sequential","Suggests",FALSE
6970
"foreach","*","base/remote","Imports",FALSE

modules/data.atmosphere/DESCRIPTION

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Description: The Predictive Ecosystem Carbon Analyzer (PEcAn) is a scientific
1919
package converts climate driver data into a standard format for models
2020
integrated into PEcAn. As a standalone package, it provides an interface to
2121
access diverse climate data sets.
22+
Depends: R (>= 4.1.0)
2223
Imports:
2324
abind (>= 1.4.5),
2425
amerifluxr,
@@ -61,6 +62,7 @@ Imports:
6162
zoo
6263
Suggests:
6364
doParallel,
65+
ecmwfr (>= 2.0.0),
6466
doSNOW,
6567
furrr,
6668
future,

modules/data.atmosphere/NAMESPACE

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,6 @@ export(wide2long)
113113
export(write_noaa_gefs_netcdf)
114114
importFrom(dplyr,"%>%")
115115
importFrom(foreach,"%dopar%")
116-
importFrom(purrr,"%>%")
117116
importFrom(rlang,.data)
118117
importFrom(rlang,.env)
119118
importFrom(sf,st_crs)

modules/data.atmosphere/NEWS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
* `ERA5_met_process()` can now process ensemble data efficiently in parallel using new option `n_cores`
44
* Dependency `ggplot2` is now suggested rather than required. It is used in two vignettes and for optional diagnostic plots from `debias_met_regression`.
5+
* `download.ERA5_cds` now uses the R package ecmwfr (replacing python dependency of cdsapi via reticulate), enabling direct NetCDF downloads; and made flexible for both reanalysis and ensemble data product.
56
* New function `sat_vapor_pressure()` added for computing saturation vapor pressure from temperature using various methods.
67

78
# PEcAn.data.atmosphere 1.9.0

modules/data.atmosphere/R/ERA5_download.R

Lines changed: 122 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -1,133 +1,149 @@
1+
#' Download ERA5 Climate Data from the Copernicus CDS API
2+
#'
13
#' @description
2-
#' This function helps to download the yearly ERA5 data based on the prescribed features using the CDS API.
3-
#' @title ERA5_cds_annual_download
4-
#'
5-
#' @param outfolder Character: physical path where the ERA5 data are stored.
4+
#' Download ERA5 climate data from the Copernicus Climate Data Store (CDS) API as NetCDF files, year by year, according to user-specified parameters.
5+
#' The function saves one NetCDF file per year in the specified output directory.
6+
#'
7+
#' @details
8+
#' This function requires a valid CDS API key and the \code{ecmwfr} package for accessing the Copernicus Climate Data Store.
9+
#' To get a Copernicus CDS API key, register at \url{https://cds.climate.copernicus.eu/profile}.
10+
#' You must provide both \code{user} (UID) and \code{key} parameters from your CDS profile.
11+
#'
12+
#' You can check the "CC-BY" license under the \href{https://cds.climate.copernicus.eu/profile?tab=licences}{'licences' tab of your profile page}.
13+
#' @param outfolder Character. Directory where downloaded NetCDF files will be saved.
614
#' @param start_date character: the start date of the data to be downloaded. Format is YYYY-MM-DD (will only use the year part of the date)
715
#' @param end_date character: the end date of the data to be downloaded. Format is YYYY-MM-DD (will only use the year part of the date)
8-
#' @param extent numeric: a vector of numbers contains the bounding box (formatted as xmin, xmax, ymin, ymax) to be downloaded.
16+
#' @param extent numeric: a vector of numbers contains the bounding box (formatted as xmin, xmax, ymin, ymax) (longitude and latitude in degrees).
917
#' @param variables character: a vector contains variables to be downloaded (e.g., c("2m_temperature","surface_pressure")).
10-
#' @param auto.create.key Boolean: decide if we want to generate the CDS RC file if it doesn't exist, the default is TRUE.
18+
#' @param time Character vector or NULL. Hours of the day to download (e.g., c("00:00", "12:00")). Default to NULL to download all hours.
19+
#' @param dataset Character. Name of the CDS dataset to use (default: "reanalysis-era5-single-levels").
20+
#' @param product_type Character. Product type to request from CDS (default: "ensemble_members").
21+
#' @param user Character. CDS user ID (UID) from your CDS profile. Required for authentication.
22+
#' @param key Character. CDS API key from your CDS profile. Required for authentication.
1123
#' @param timeout numeric: the maximum time (in seconds) allowed to download the data. The default is 36000 seconds.
1224
#'
13-
#' @return A vector containing file paths to the downloaded files.
25+
#' @return
26+
#' A list where each element is a list containing:
27+
#' \item{file}{File path to the downloaded NetCDF file.}
28+
#' \item{host}{Host name where the file was downloaded.}
29+
#' \item{startdate}{Start date and time of the data in the file.}
30+
#' \item{enddate}{End date and time of the data in the file.}
31+
#' \item{mimetype}{MIME type of the file ("application/x-netcdf").}
32+
#' \item{formatname}{Format name ("ERA5_year.nc").}
33+
#'
34+
#' @examples
35+
#' \dontrun{
36+
#' # Download ERA5 reanalysis data for 2020
37+
#' output_dir <- withr::local_tempdir()
38+
#' era5_files <- download.ERA5_cds(
39+
#' outfolder = output_dir,
40+
#' start_date = "2020-01-01",
41+
#' end_date = "2020-06-30",
42+
#' extent = c(-72.2215, -72.1215, 42.4878, 42.5878),
43+
#' variables = c("2m_temperature", "surface_pressure"),
44+
#' user = "your_cds_user_id",
45+
#' key = "your_cds_api_key",
46+
#' product_type = "reanalysis"
47+
#' )
48+
#'
49+
#' # Download ensemble data for specificed hours only
50+
#' era5_files <- download.ERA5_cds(
51+
#' outfolder = output_dir,
52+
#' start_date = "2020-01-01",
53+
#' end_date = "2020-12-31",
54+
#' extent = c(-83.05, -82.95, 42.95, 43.05),
55+
#' variables = "surface_solar_radiation_downwards",
56+
#' user = "your_cds_user_id",
57+
#' key = "your_cds_api_key",
58+
#' time = c("00:00", "12:00")
59+
#' )
60+
#' }
1461
#' @export
1562
#'
16-
#' @importFrom purrr %>%
17-
#' @author Dongchen Zhang
18-
download.ERA5_cds <- function(outfolder, start_date, end_date, extent, variables, auto.create.key = T, timeout = 36000) {
19-
# check shell environments.
20-
if ("try-error" %in% class(try(system("grib_to_netcdf"), silent = T))) {
21-
PEcAn.logger::logger.info("The grib_to_netcdf function is not detected in shell command.")
22-
return(NA)
63+
#' @author Dongchen Zhang, Akash
64+
65+
download.ERA5_cds <- function(outfolder, start_date, end_date,
66+
extent, variables, user, key, time = NULL,
67+
dataset = "reanalysis-era5-single-levels",
68+
product_type = "ensemble_members",
69+
timeout = 36000) {
70+
71+
# check for required package
72+
if (!requireNamespace("ecmwfr", quietly = TRUE)) {
73+
PEcAn.logger::logger.severe(
74+
"Package 'ecmwfr' is required for ERA5 downloads. ",
75+
"Install with: install.packages('ecmwfr'). ",
76+
"Get CDS credentials from: https://cds.climate.copernicus.eu/profile"
77+
)
2378
}
79+
80+
if (!dir.exists(outfolder)) dir.create(outfolder, recursive = TRUE)
81+
2482
# setup timeout for download.
2583
options(timeout=timeout)
2684
# convert arguments to CDS API specific arguments.
2785
years <- sort(unique(lubridate::year(seq(lubridate::date(start_date), lubridate::date(end_date), "1 year"))))
28-
months <- sort(unique(lubridate::month(seq(lubridate::date(start_date), lubridate::date(end_date), "1 month")))) %>%
86+
months <- sort(unique(lubridate::month(seq(lubridate::date(start_date), lubridate::date(end_date), "1 month")))) |>
2987
purrr::map(function(d)sprintf("%02d", d))
30-
days <- sort(unique(lubridate::day(seq(lubridate::date(start_date), lubridate::date(end_date), "1 day")))) %>%
88+
days <- sort(unique(lubridate::day(seq(lubridate::date(start_date), lubridate::date(end_date), "1 day")))) |>
3189
purrr::map(function(d)sprintf("%02d", d))
32-
times <- list('00:00','03:00','06:00',
33-
'09:00','12:00','15:00',
34-
'18:00','21:00')
35-
area <- paste(c(extent[4], extent[1], extent[3], extent[2]), collapse = "/")
36-
variables <- as.list(variables)
37-
#load cdsapi from python environment.
38-
tryCatch({
39-
cdsapi <- reticulate::import("cdsapi")
40-
}, error = function(e) {
41-
PEcAn.logger::logger.severe(
42-
"Failed to load `cdsapi` Python library. ",
43-
"Please make sure it is installed to a location accessible to `reticulate`.",
44-
"You should be able to install it with the following command: ",
45-
"`pip install --user cdsapi`.",
46-
"The following error was thrown by `reticulate::import(\"cdsapi\")`: ",
47-
conditionMessage(e)
48-
)
49-
})
50-
#define function for building credential file.
51-
#maybe as a helper function.
52-
getnetrc <- function (dl_dir) {
53-
netrc <- file.path(dl_dir, ".cdsapirc")
54-
if (file.exists(netrc) == FALSE ||
55-
any(grepl("https://cds.climate.copernicus.eu/api/v2",
56-
readLines(netrc))) == FALSE) {
57-
netrc_conn <- file(netrc)
58-
writeLines(c(
59-
sprintf(
60-
"url: %s",
61-
getPass::getPass(msg = "Enter URL from the following link \n (https://cds.climate.copernicus.eu/api-how-to#install-the-cds-api-key):")
62-
),
63-
sprintf(
64-
"key: %s",
65-
getPass::getPass(msg = "Enter KEY from the following link \n (https://cds.climate.copernicus.eu/api-how-to#install-the-cds-api-key):")
66-
)
67-
),
68-
netrc_conn)
69-
close(netrc_conn)
70-
message(
71-
"A netrc file with your CDS Login credentials was stored in the output directory "
72-
)
73-
}
74-
return(netrc)
90+
91+
# handle time argument: all hours if Null
92+
if (is.null(time)) {
93+
times <- sprintf("%02d:00", 0:23)
94+
} else {
95+
times <- time
7596
}
76-
#check if the token exists for the cdsapi.
77-
if (!file.exists(file.path(Sys.getenv("HOME"), ".cdsapirc")) & auto.create.key) {
78-
if ("try-error" %in% class(try(find.package("getPass")))) {
79-
PEcAn.logger::logger.info("The getPass pacakge is not installed for creating the API key.")
80-
return(NA)
81-
} else {
82-
getnetrc(Sys.getenv("HOME"))
83-
}
84-
} else if (!file.exists(file.path(Sys.getenv("HOME"), ".cdsapirc")) & !auto.create.key) {
97+
98+
# Format area for CDS API (North, West, South, East)
99+
area <- round(c(extent[4], extent[1], extent[3], extent[2]), 2)
100+
variables <- as.list(variables)
101+
102+
# Set CDS credentials
103+
if (is.null(user) || is.null(key)) {
85104
PEcAn.logger::logger.severe(
86-
"Please create a `${HOME}/.cdsapirc` file as described here:",
87-
"https://cds.climate.copernicus.eu/api-how-to#install-the-cds-api-key ."
105+
"CDS 'user' and 'key' must be provided. ",
106+
"Get them from: https://cds.climate.copernicus.eu/profile"
88107
)
89108
}
90-
#grab the client object.
91-
tryCatch({
92-
c <- cdsapi$Client()
93-
}, error = function(e) {
94-
PEcAn.logger::logger.severe(
95-
"The following error was thrown by `cdsapi$Client()`: ",
96-
conditionMessage(e)
97-
)
98-
})
109+
ecmwfr::wf_set_key(user = user, key = key)
110+
99111
# loop over years.
100112
nc.paths <- c()
101113
for (y in years) {
102-
fname <- file.path(outfolder, paste0("ERA5_", y, ".grib"))
103-
# start retrieving data.
104-
# you need to have an account for downloaing the files
105-
# Read the documantion for how to setup your account and settings before trying this
106-
# https://confluence.ecmwf.int/display/CKB/How+to+download+ERA5#HowtodownloadERA5-3-DownloadERA5datathroughtheCDSAPI
107-
c$retrieve(
108-
'reanalysis-era5-single-levels',
109-
list(
110-
'product_type' = 'ensemble_members',
111-
'data_format' = 'grib',
112-
"download_format" = "unarchived",
113-
'day' = days,
114-
'time' = times,
115-
'month' = months,
116-
'year' = as.character(y),
117-
"area" = area,
118-
'variable' = variables
119-
),
120-
fname
114+
fname <- file.path(outfolder, paste0("ERA5_", y, ".nc"))
115+
116+
request <- list(
117+
dataset_short_name = dataset,
118+
product_type = list(product_type),
119+
data_format = 'netcdf',
120+
download_format = "unarchived",
121+
day = days,
122+
time = times,
123+
month = months,
124+
year = list(as.character(y)),
125+
area = area,
126+
variable = variables,
127+
target = basename(fname)
121128
)
122-
# convert grib to nc file.
123-
nc.path <- gsub(".grib", ".nc", fname, fixed = T)
124-
cmd <- paste("grib_to_netcdf", fname, "-o", nc.path)
125-
out <- system(cmd, intern = F, ignore.stdout = T, ignore.stderr = T)
126-
# store the path.
127-
nc.paths <- c(nc.paths, nc.path)
128-
# remove previous grib file.
129-
unlink(fname)
129+
130+
# Submit request using ecmwfr
131+
tryCatch({
132+
ecmwfr::wf_request(
133+
request = request,
134+
user = user,
135+
path = outfolder,
136+
time_out = timeout
137+
)
138+
nc.paths <- c(nc.paths, fname)
139+
}, error = function(e) {
140+
PEcAn.logger::logger.error(
141+
"Failed to download data for year ", y, ": ",
142+
conditionMessage(e)
143+
)
144+
})
130145
}
146+
131147
# construct results to meet the requirements of pecan.met workflow.
132148
results <- vector("list", length = length(years))
133149
for (i in seq_along(results)) {

0 commit comments

Comments
 (0)