Skip to content

Commit ec30651

Browse files
authored
Merge pull request #3620 from DongchenZ/SDA_model
Adding functions to merge netCDF files from pecan model outputs.
2 parents b9b28d9 + fa14284 commit ec30651

18 files changed

Lines changed: 482 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ section for the next release.
1313
* Add CH4 and N2O to standard_vars in PEcAn.utils
1414
* New function `sat_vapor_pressure()` added for computing saturation vapor pressure from temperature using various methods.
1515
* Added `AmeriFlux_met_ensemble()` function with ERA5 fallback for AmeriFlux meteorological data processing and ensemble generation
16+
* Added `all_site_nc_merge_by_year()` and `single_site_nc_merge()` functions to merge netCDF files across ensembles and sites from pecan model netCDF outputs.
17+
1618

1719
### Fixed
1820

base/utils/DESCRIPTION

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ Imports:
3737
abind (>= 1.4.5),
3838
curl,
3939
dplyr,
40+
foreach,
4041
lubridate (>= 1.6.0),
4142
magrittr,
4243
ncdf4 (>= 1.15),
@@ -48,6 +49,7 @@ Imports:
4849
Suggests:
4950
coda (>= 0.18),
5051
data.table,
52+
doSNOW,
5153
ggplot2,
5254
MASS,
5355
mockery,

base/utils/NAMESPACE

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export(distn.stats)
1515
export(distn.table.stats)
1616
export(download.url)
1717
export(download_file)
18+
export(extract_nc_sda)
1819
export(full.path)
1920
export(get.ensemble.inputs)
2021
export(get.parameter.stat)
@@ -32,6 +33,7 @@ export(misc.are.convertible)
3233
export(misc.convert)
3334
export(mstmipvar)
3435
export(n_leap_day)
36+
export(nc_merge_all_sites_by_year)
3537
export(nc_write_varfiles)
3638
export(need_packages)
3739
export(paste.stats)
@@ -63,5 +65,6 @@ export(units_are_equivalent)
6365
export(vecpaste)
6466
export(zero.bounded.density)
6567
export(zero.truncate)
68+
importFrom(foreach,"%dopar%")
6669
importFrom(magrittr,"%>%")
6770
importFrom(rlang,.data)
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
#' Combine many netCDFs into one file per year
2+
#'
3+
#' Merges model outputted netCDF files by the time steps specified in a pecan settings file.
4+
#'
5+
#' The function is only tested for SIPNET model runs that were run with state data assimilation enabled.
6+
#' Please make sure you have the same netCDF formats if you want to proceed with different models.
7+
#' We could also have more functions that deal with different dimensions (e.g., by site instead of by year).
8+
#'
9+
#' @param model.outdir character: path to the folder that contains model outputs.
10+
#' @param nc.outdir character: physical path to the folder that contains the merged netCDF files.
11+
#' @param ens.num numeric: number of ensembles for the model run.
12+
#' @param site.ids numeric or character: vector of site ids across locations.
13+
#' @param start.date date or character in YYYY-MM-DD format: start date of the model run.
14+
#' @param end.date date or character in YYYY-MM-DD format: end date of the model run.
15+
#' @param time.step character: time step of the model run. Default is 1 year.
16+
#' @param cores numeric: the number of CPUs for the parallel computation. Default is 1.
17+
#'
18+
#' @return character: file paths to the merged netCDF files.
19+
#' @export
20+
#'
21+
#' @author Dongchen Zhang
22+
#' @importFrom magrittr %>%
23+
#' @importFrom foreach %dopar%
24+
nc_merge_all_sites_by_year <- function (model.outdir,
25+
nc.outdir,
26+
ens.num,
27+
site.ids,
28+
start.date,
29+
end.date,
30+
time.step = "1 year",
31+
cores = 1) {
32+
# check shell environments.
33+
if (suppressWarnings(system2("which", "cdo", stdout = FALSE)) != 0) {
34+
PEcAn.logger::logger.info("The cdo function is not detected in shell command.")
35+
return(NA)
36+
}
37+
# create the nc output folder if it doesn't exist.
38+
if (!file.exists(nc.outdir)) {
39+
dir.create(nc.outdir)
40+
}
41+
# calculate time points.
42+
time.points <- lubridate::year(seq(lubridate::date(start.date),
43+
lubridate::date(end.date),
44+
time.step))
45+
46+
# loop over time.
47+
# initialize parallel.
48+
cl <- parallel::makeCluster(as.numeric(cores))
49+
doSNOW::registerDoSNOW(cl)
50+
#progress bar
51+
pb <- utils::txtProgressBar(min = 1, max = length(site.ids), style = 3)
52+
progress <- function(n) utils::setTxtProgressBar(pb, n)
53+
opts <- list(progress=progress)
54+
# record nc paths.
55+
nc.paths <- c()
56+
for (t in seq_along(time.points)) {
57+
time <- time.points[t] # grab the current time point.
58+
# record previous file.
59+
if (file.exists(file.path(nc.outdir, paste0(time, ".nc")))) {
60+
nc.paths <- c(nc.paths, file.path(nc.outdir, paste0(time, ".nc")))
61+
next
62+
}
63+
# loop over sites.
64+
s <- NULL # For passing the GitHub actions.
65+
nc.files <-
66+
foreach::foreach(s = seq_along(site.ids),
67+
.packages = c("purrr", "ncdf4"),
68+
.options.snow=opts) %dopar% {
69+
nc_merge_single_site(model.outdir = model.outdir,
70+
nc.outdir = nc.outdir,
71+
ens.num = ens.num,
72+
# cdo collgrid only works for numeric data type.
73+
site.id = site.ids[s],
74+
time)
75+
} %>% unlist
76+
# merge across sites using CDO command.
77+
cmd <- "cdo -P @CORES@ collgrid @NC.OUTDIR@/*@TIME@.nc @OUTFILE@"
78+
cmd <- gsub("@CORES@", cores, cmd)
79+
cmd <- gsub("@NC.OUTDIR@", nc.outdir, cmd)
80+
cmd <- gsub("@TIME@", time, cmd)
81+
cmd <- gsub("@OUTFILE@", file.path(nc.outdir, paste0(time, ".nc")), cmd)
82+
out <- system(cmd, intern = TRUE, ignore.stdout = TRUE, ignore.stderr = TRUE)
83+
# if we have site ids in character format.
84+
if (all(is.character(site.ids))) {
85+
nc <- ncdf4::nc_open(file.path(nc.outdir, paste0(time, ".nc")))
86+
site_dim <- ncdf4::ncdim_def("site", units = "", vals = seq_along(site.ids))
87+
site_id_var <- ncdf4::ncvar_def("site_id", units = "", dim = site_dim, prec = "char")
88+
ncdf4::ncvar_put(nc, varid = "site_id", vals = site.ids)
89+
ncdf4::nc_close(nc) # close nc connection.
90+
}
91+
# record the current nc path.
92+
nc.paths <- c(nc.paths, file.path(nc.outdir, paste0(time, ".nc")))
93+
# remove nc files for each site.
94+
unlink(nc.files)
95+
}
96+
# stop parallel.
97+
parallel::stopCluster(cl)
98+
foreach::registerDoSEQ()
99+
# return nc paths.
100+
return(nc.paths)
101+
}
102+
103+
#' Merge model outputted netCDF files across ensembles for a single site.
104+
#' @details
105+
#' The function is only tested for SIPNET model runs.
106+
#' Please make sure you have the same netCDF formats if you want to proceed with different models.
107+
#'
108+
#' This function requires `site.id` to be an integer.
109+
#' If your sites have non-numeric IDs, one possible workaround is to
110+
#' pass a dummy value and then edit the output file afterward to replace its `site_id` variable
111+
#' with character data. If you do this, do be aware many legacy netCDF tools have poor support
112+
#' for netCDFs containing character data.
113+
#'
114+
#' @param model.outdir character: physical path to the model output folder.
115+
#' @param nc.outdir character: physical path to the folder that contains the merged netCDF files.
116+
#' @param ens.num numeric: ensemble size.
117+
#' @param site.id numeric: identification number of the site.
118+
#' @param time numeric or character: the current time of netCDF files to be extracted.
119+
#' See details for use with non-numeric siteIDs
120+
#' @return character: file path to the merged netCDF file.
121+
#'
122+
#' @author Dongchen Zhang
123+
nc_merge_single_site <- function (model.outdir, nc.outdir, ens.num, site.id, time) {
124+
# grab basic formats from the first nc file of the site.
125+
# create the folder name associated with first ensemble and first site.
126+
prefix <- "ENS-"
127+
folder.name <- paste0(prefix, sprintf("%05d", 1), "-", site.id)
128+
# read nc file.
129+
nc <- ncdf4::nc_open(file.path(model.outdir, folder.name, paste0(time, ".nc")))
130+
nc.vars <- nc$var # grab variable definitions.
131+
time.values <- nc$dim$time # grab time dimensions.
132+
lat <- nc$dim$lat$vals
133+
lon <- nc$dim$lon$vals
134+
ncdf4::nc_close(nc) # close nc connection.
135+
# dimension and variable definitions.
136+
# site dimension.
137+
site_dim <- ncdf4::ncdim_def("site", units = "", vals = site.id)
138+
# time dimension.
139+
time_dim <- ncdf4::ncdim_def("time", longname = "time", units = time.values$units, vals = time.values$vals)
140+
# ensemble dimension.
141+
ens_dim <- ncdf4::ncdim_def("ensemble", longname = "ensemble member", unit = "", vals = 1:ens.num)
142+
# define site-specific variables.
143+
lat_var <- ncdf4::ncvar_def("latitude", units = "degrees_north", dim = site_dim, prec = "double")
144+
lon_var <- ncdf4::ncvar_def("longitude", units = "degrees_east", dim = site_dim, prec = "double")
145+
site_id_var <- ncdf4::ncvar_def("site_id", units = "", dim = site_dim, prec = "integer")
146+
# loop over variables.
147+
first.creation <- TRUE
148+
for (i in seq_along(nc.vars)) {
149+
# grab the variable name.
150+
var <- nc.vars[[i]]$name
151+
# skip if it's time related variable.
152+
if (grepl("time", var, fixed = T)) next
153+
# loop over ensembles.
154+
var.mat <- matrix(NA, time.values$len, ens.num)
155+
for (ens in 1:ens.num) {
156+
# TODO: add checks to make sure every thing in files are in the same shape and format.
157+
folder.name <- paste0(prefix, sprintf("%05d", ens), "-", site.id)
158+
nc <- ncdf4::nc_open(file.path(model.outdir, folder.name, paste0(time, ".nc")))
159+
var.mat[,ens] <- ncdf4::ncvar_get(nc, var = var)
160+
ncdf4::nc_close(nc)
161+
}
162+
# define the current model variable.
163+
temp_var <- ncdf4::ncvar_def(nc.vars[[i]]$name,
164+
units = nc.vars[[i]]$units,
165+
dim = list(site_dim, ens_dim, time_dim),
166+
prec = nc.vars[[i]]$prec)
167+
# if it's the first variable, we will need to create the NC file along with the site-specific variables.
168+
if (first.creation) {
169+
# turn the flag off.
170+
first.creation <- !first.creation
171+
# create nc file.
172+
nc_file <- ncdf4::nc_create(file.path(nc.outdir, paste0(site.id, "_", time, ".nc")), list(site_id_var, lon_var, lat_var, temp_var))
173+
# add the site-specific variables.
174+
ncdf4::ncvar_put(nc_file, varid = "site_id", vals = site.id)
175+
ncdf4::ncvar_put(nc_file, varid = "latitude", vals = lat)
176+
ncdf4::ncvar_put(nc_file, varid = "longitude", vals = lon)
177+
# add the current variable.
178+
ncdf4::ncvar_put(nc_file, varid = nc.vars[[i]]$name, vals = var.mat)
179+
} else {
180+
# add additional variable.
181+
nc_file <- ncdf4::ncvar_add(nc_file, temp_var)
182+
# update data.
183+
ncdf4::ncvar_put(nc_file, varid = nc.vars[[i]]$name, vals = var.mat)
184+
}
185+
}
186+
# close nc connection.
187+
ncdf4::nc_close(nc_file)
188+
# return all nc paths.
189+
return(file.path(nc.outdir, paste0(site.id, "_", time, ".nc")))
190+
}
191+
192+
#' Extract netCDF file by site.id, time window, and variable name.
193+
#' @details
194+
#' The function is only tested for netCDF files generated by the `nc_merge_all_sites_by_year` function.
195+
#'
196+
#' @param site.id numeric or character: identification of the site.
197+
#' @param start.date date in YYYY-MM-DD format: start date of the requested time window.
198+
#' @param end.date date in YYYY-MM-DD format: end date of the requested time window.
199+
#' @param var.name character: variable name.
200+
#' @param nc.path character: physical path to the target netCDF file.
201+
#'
202+
#' @return list: a list contains requested array, time steps, site id, variable name, and ensemble size.
203+
#'
204+
#' @author Dongchen Zhang
205+
#' @export
206+
extract_nc_sda <- function (site.id, start.date, end.date, var.name, nc.path) {
207+
# open NC file.
208+
nc <- ncdf4::nc_open(nc.path)
209+
# grab the index for the requested site.id.
210+
site.ind <- which(nc$dim$site$vals == site.id)
211+
# calculate real time.
212+
time.val <- nc$dim$time$vals
213+
time.unit <- nc$dim$time$units
214+
origin <- strsplit(x = time.unit, split = "since ", fixed = TRUE)[[1]][2]
215+
real_time <- as.POSIXct(time.val*3600*24, origin = origin, tz = "UTC")
216+
time.steps <- length(real_time)
217+
# grab ensemble size.
218+
ensemble.size <- nc$dim$ensemble$len
219+
# if we have the time window.
220+
if (start.date >= real_time[1] & end.date <= real_time[time.steps]) {
221+
time.inds <- which(real_time >= start.date & real_time <= end.date)
222+
} else {
223+
PEcAn.logger::logger.info("The netCDF file doesn't include the date range you asking for.")
224+
return(0)
225+
}
226+
# grab outputs.
227+
res <- ncdf4::ncvar_get(nc, var.name, start = c(time.inds[1], site.ind, 1), count = c(length(time.inds), 1, ensemble.size))
228+
# close NC connection.
229+
ncdf4::nc_close(nc)
230+
# prepare outputs.
231+
return(list(mat = res, time.points = real_time[time.inds], site.ids = site.id, var.name = var.name, ensemble.size = ensemble.size))
232+
}

base/utils/man/extract_nc_sda.Rd

Lines changed: 31 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

base/utils/man/nc_merge_all_sites_by_year.Rd

Lines changed: 48 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)