|
| 1 | +#' Download ERA5 Climate Data from the Copernicus CDS API |
| 2 | +#' |
1 | 3 | #' @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. |
6 | 14 | #' @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) |
7 | 15 | #' @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). |
9 | 17 | #' @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. |
11 | 23 | #' @param timeout numeric: the maximum time (in seconds) allowed to download the data. The default is 36000 seconds. |
12 | 24 | #' |
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 | +#' } |
14 | 61 | #' @export |
15 | 62 | #' |
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 | + ) |
23 | 78 | } |
| 79 | + |
| 80 | + if (!dir.exists(outfolder)) dir.create(outfolder, recursive = TRUE) |
| 81 | + |
24 | 82 | # setup timeout for download. |
25 | 83 | options(timeout=timeout) |
26 | 84 | # convert arguments to CDS API specific arguments. |
27 | 85 | 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")))) |> |
29 | 87 | 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")))) |> |
31 | 89 | 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 |
75 | 96 | } |
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)) { |
85 | 104 | 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" |
88 | 107 | ) |
89 | 108 | } |
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 | + |
99 | 111 | # loop over years. |
100 | 112 | nc.paths <- c() |
101 | 113 | 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) |
121 | 128 | ) |
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 | + }) |
130 | 145 | } |
| 146 | + |
131 | 147 | # construct results to meet the requirements of pecan.met workflow. |
132 | 148 | results <- vector("list", length = length(years)) |
133 | 149 | for (i in seq_along(results)) { |
|
0 commit comments