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+ }
0 commit comments