Introduction

This document shows how to generate domain and surface files for a single point of interest. This is needed for when setting a new site. Currently this script simply uses existing domain and surface files, extracts information from the nearest grid cell, and complements with site-specific information you might have.

Reset session

Use this chunk to fully reset R.

# Unload all packages except for the R default ones
plist = names(sessionInfo()$otherPkgs)
if (length(plist) > 0){
   dummy = sapply(X=paste0("package:",plist),FUN=detach,character.only=TRUE,unload=TRUE)
}#end if (length(plist) > 0)


# Remove all variables
rm(list=ls())

# Reset warnings
options(warn=0)

# Close all plots
invisible(graphics.off())

# Clean up
invisible(gc())

Main settings

Required packages

Load some packages. By default, we skip the warning messages, but it may be a good idea to visualise them if it is the first time running.

isfine = c( data.table = require(data.table,quietly=TRUE,warn.conflicts=FALSE)
          , fields     = require(fields    ,quietly=TRUE,warn.conflicts=FALSE)
          , lubridate  = require(lubridate ,quietly=TRUE,warn.conflicts=FALSE)
          , ncdf4      = require(ncdf4     ,quietly=TRUE,warn.conflicts=FALSE)
          , purrr      = require(purrr     ,quietly=TRUE,warn.conflicts=FALSE)
          , reshape2   = require(reshape2  ,quietly=TRUE,warn.conflicts=FALSE)
          , soilDB     = require(soilDB    ,quietly=TRUE,warn.conflicts=FALSE)
          , terra      = require(terra     ,quietly=TRUE,warn.conflicts=FALSE)
          , tidyverse  = require(tidyverse ,quietly=TRUE,warn.conflicts=FALSE)
          )#end c
## Warning: package 'ncdf4' was built under R version 4.3.3
## Warning: package 'soilDB' was built under R version 4.3.3
if (! all(isfine)){
   cat0(" List of required packages, and the success status loading them:\n")
   print(isfine)
   stop(" Some packages are missing and must be installed.")
}

Path and file location

Set paths and files for input and output.

  • here_path. The main path for processing data. The output path will be generated here.
  • input_path. The location where existing surface and domain netcdf files are located
  • output_path. The main output path for the data. A sub-directory for this site will be created.
here_path   = getwd()
input_path  = file.path(path.expand("~"),"Documents","LocalData","TowerData","GriddedData")
output_path = file.path(path.expand("~"),"Data","FATES_DataSets")

Set the URL for the input surface data and soil texture file. These are normally a gridded data file, not single-site files. These files are large and the servers can be slow, so we will only download the data in case we cannot locate the corresponding files at input_path.

  • url_surfgrid. This is the surface data, in NetCDF format. Normally we use a surface file retrieved from either the CESM or the E3SM archive.
  • url_soildepth. This is the soil depth data, in GeoTIFF format. By default this is the Shangguan et al. (2017), available at the ISRIC website as part of the original SoilGrids data base (version 1). Note that SoilGrids 2.0 no longer provides soil depth although according to their FAQ they plan to release it at some point, in which case this may be replaced.

If you have your own set of input surface or soil depth data, set a dummy URL with the actual file name. For example, if you have the surface file my_surface_grid_file.nc, set url.surfgrid="https://localhost/my_surface_grid_file.nc".

# Gridded surface file
url_surfgrid = "https://svn-ccsm-inputdata.cgd.ucar.edu/trunk/inputdata/lnd/clm2/surfdata_map/release-clm5.0.24/surfdata_0.125x0.125_hist_16pfts_Irrig_CMIP6_simyr2005_c190613.nc"

# Gridded depth to bedrock
url_soildepth = "https://files.isric.org/soilgrids/former/2017-03-10/data/BDTICM_M_250m_ll.tif"

Author information

Provide information about yourself. This will be included in the netCDF headers. In case you do not want to specify any of them, set them to NA_character_

  • author_name. Person who is generating these files (typically it’s you).
  • author_email. Contact email (so people can reach out to you if problems arise).
author_name  = "This Is You"
author_email = "yourself@somewhere.gov"

Site information

Provide some basic information about the site:

  • xid. Unique site ID.
  • site_desc. Site description (to be added to the NetCDF files headers).
  • site_lon. Longitude (degrees east).
  • site_lat. Latitude (degrees north).
  • site_alt. Altitude (metres above sea level).
  • dxy. Intended grid size.
  • site_pcsand; site_pcclay. Percentage of sand and clay, respectively. They should be both be limited between 0 and 100., and their sum shall not exceed 100. Silt fraction will be derived from these fractions. If these fractions are not known, set them to NA_real_. Provide either a single value (for all layers) or a vector with values for each soil layer (defined by site_depth). If the texture should be obtained from SoilGrids2.0, set one of the variables to "SoilGrids" (case insensitive).
  • site_organic. Soil organic content \(\left[\mathrm{kg\,m^{-3}}\right]\). If these fractions are not known, set them to NA_real_. Provide either a single value (for all layers) or a vector with values for each soil layer (defined by site_depth). If the soil organic content should be obtained from SoilGrids2.0, set one of the variables to "SoilGrids" (case insensitive).
  • site_zbot. This is used only if site_pcsand, site_pcclay or site_organic are vectors. In this case, this value provides the soil depth at the bottom of each layer for which site-specific soil information is provided \(\left[\mathrm{m}\right]\). The top of the soil layers will match the bottom of the previous layer, and the top of the first layer will be assumed to be at the surface (i.e., at site_ztop=0). If no site-specific information is provided (i.e., if site_zbot=NA_real_), we assume that the input is provided at the same layers as the host land models.
    • Important: This will not reset the soil layer depths in the host land model. Instead, this will be used for interpolating the site-specific quantities onto the host land model soil depth layers.
    • This will be ignored if retrieving data from SoilGrids 2.0.
  • site_bedrock. Soil depth to bedrock. If the value is valid and numeric, this will be used as the soil depth. To use the gridded dataset provided in url_soildepth, set variable site_bedrock to "Gridded" (case insensitive). The code will reset the number of active soil layers to the one whose top is still above the bedrock layer. In case soil depth is unknown, leave it as NA_real_, in which case the code will not change the default thickness of the active layer.
  • br_scale. If soil depth is provided either as a number or through the GeoTIFF, this variable can be used for converting the original units to metres \(\left[\mathrm{m}\right]\). For example, if using the Shangguan et al. (2017), the values are originally in \(\left[\mathrm{cm}\right]\), in which case you must set up bedrock_scale = 0.01
  • br_window. Which window size \(\left[\mathrm{m}\right]\) around the nearest pixel should we consider for sampling of soil depth to bedrock, in case site_bedrock="Gridded". If the code should sample only the actual pixel, set br_window=NA_real_.
  • undef. A number to denote missing values. This is needed by NetCDF, but the data should not have any.
  • force_natural. Should we overwrite the fraction cover of global gridded data set, so natural vegetation becomes 100%?
xid           = "paracouGUF"
site_desc     = "Guyaflux tower at Paracou, French Guiana"
dat_version   = "3.1"
site_lon      = -52.912222
site_lat      =   5.281667
site_alt      =  30.000000
dxy           =  1.0
site_xsand    = "SoilGrids" 
site_xclay    = "SoilGrids" 
site_organic  = "SoilGrids"
site_zbot     = NA_real_
site_bedrock  = "Gridded"
br_scale      = 0.01       # Unit conversion scale for depth to bedrock
br_window     = 2500.      # Buffer size for the pixel extraction
undef         = -9999.99
force_natural = TRUE

Additional settings for retrieving the surface gridded data

  • pcveg_min. Minimum percentage of natural vegetation to consider when loading surface grid. This can be useful to avoid selecting a grid cell over non-natural vegetation when using a site in heavily degraded areas.
  • landfrac_min. Minimum fraction of land to consider when loading surface grid. This can be useful to avoid selecting a grid cell over the ocean when using a coastal site.
pcveg_min    = 8.00
landfrac_min = 0.50

Please read this before changing the settings in the next chunk. This allows for changing the soil layers settings in the surface file. As of 2025-05-01, there is a pull request in E3SM that enables non-standard soil layers in ELM. As of 2025-05-01, this is not available in CLM. This message may be revised if ELM integrates the pull request and CLM implements a similar configuration.

  • user_def_zsoi. Use the default ELM/CLM soil layers (user_def_zsoi=FALSE) or specify a different soil layer grid (user_def_zsoi_TRUE). Unless you use the code available in the ELM pull request (or ELM if this pull request is merged), you know what you are doing, and understand the consequences of changing from defaults, we strongly recommend sticking with user_def_zsoi=FALSE
  • user_nlevgrnd. The user-defined number of soil layers. This must not exceed the default in the land model. Currently both ELM and CLM allow for 15 layers. If you need more than 15 layers, you will need to change the host land model code.
  • user_nlevsoi. The default number of active soil layers. This cannot exceed the default nlevgrnd in the model, but it can be less, in which case some soil layers will not be hydrologically active.
  • user_scalez. The scaling factor for the exponential term used to calculate the layer thickness.
  • user_zecoeff. The exponent coefficient for increasing soil layer depth for deeper layers.
# Set soil layer parameters
user_def_zsoi = c(FALSE,TRUE)[1L] # Process soil using the user-defined settings?
                                  # - Leave this set as FALSE unless you know what you are
                                  #   doing and the consequences of setting it to TRUE
user_nlevgrnd = 15L               # User-defined number of soil layers
user_nlevsoi  = 15L               # User-defined (maximum) number of active soil layers
user_scalez   = 0.030             # User-defined scaling factor for soil layers
user_zecoeff  = 0.45              # User-defined exponential coefficient for soil layers

This concludes the initial settings. From this point on, you may not need to change anything, unless you are debugging or adding new features.

Data processing

Check inputs

Ensure the input and output paths

dummy = dir.create(input_path ,showWarnings=FALSE,recursive=TRUE)

First, we check whether the input surface data file is already available at the input path. In case it is not, we download it. Because some of the files may be very large, we temporarily expand R’s default timeout for the operation to one day.

# Find base name for soil depth
input_surfgrid = file.path(input_path,basename(url_surfgrid))

# Download data in case they are not found.  Often these files are huge, so we tempo
if (! file.exists(input_surfgrid)){
  orig.timeout = getOption("timeout")
  dummy = options(timeout=86400)
  dummy = download.file(url=url_surfgrid,destfile=input_surfgrid)
  dummy = options(timeout=orig.timeout)
}#end if (! file.exists(input_surfgrid))

if (tolower(site_bedrock) %in% "gridded"){
   # Find base name for soil depth
   input_soildepth = file.path(input_path,basename(url_soildepth))

   # Download data in case they are not found.  Often these files are huge, so we tempo
   if (! file.exists(input_soildepth)){
      orig.timeout = getOption("timeout")
      dummy = options(timeout=86400)
      dummy = download.file(url=url_soildepth,destfile=input_soildepth)
      dummy = options(timeout=orig.timeout)
   }#end if (! file.exists(input_surfgrid))
}#end if (tolower(site_bedrock) %in% "gridded")

Output names

Build the output file names, following the ELM/CLM convention.

# Labels for describing the data set.
ymd_now = today(tzone = "UTC")
ymd_lab = sprintf("%4.4i%2.2i%2.2i",year(ymd_now),month(ymd_now),day(ymd_now))

# Tag specific for this site, grid, and version (used for file and path names).
site_tag  = paste0("1x1pt-",xid,"_v",dat_version,"_c",ymd_lab)

#    Set output path to be the same as the site tag.
site_path   = file.path(output_path,site_tag)
dummy       = dir.create(site_path  ,recursive=TRUE,showWarnings=FALSE)

# Domain file.  In case the file exists, remove it.
output_domain   = file.path(site_path,paste0("domain.lnd.",site_tag,"_navy.nc"))
if (file.exists(output_domain)) file.remove(output_domain)
## [1] TRUE
# Surface data file
output_surfdata = file.path(site_path,paste0("surfdata_",site_tag,".nc"))
if (file.exists(output_surfdata)) file.remove(output_surfdata)

Define function that calculates soil depths for the surface file

This defines the middle, top and bottom of the soil layers using the default settings in ELM. This is needed because the default surface files for ELM and CLM do not contain the soil grid, and cannot be used for modifying the soil grid. ELM does not have a straightforward way of modifying the soil profile for now, so we always assume the default soil.

# Define the original CLM/ELM settings
orig_nlevgrnd = 15L
orig_nlevsoi  = 10L
orig_scalez   = 0.025
orig_zecoeff  = 0.50

# Decide whether to use default soil layers or not:
if (user_def_zsoi){
   nlevgrnd = user_nlevgrnd
   nlevsoi  = user_nlevsoi
   scalez   = user_scalez
   zecoeff  = user_zecoeff
}else{
   # Use the standard CLM/ELM parameters
   nlevgrnd = 15L
   nlevsoi  = 10L
   scalez   = 0.025
   zecoeff  = 0.50
}#end if (user_def_zsoi)

# Middle of the soil layer
active         = sequence(nlevsoi)
zsoi           = rep(NA_real_,times=nlevgrnd)
zsoi[active]   = scalez * ( exp( zecoeff * (active-0.5) ) - 1.0 )
znlevsoi       = zsoi[nlevsoi]
if (nlevsoi < nlevgrnd){
   inactive       = seq(from=nlevsoi+1L,to=nlevgrnd,by=1L)
   zsoi[inactive] = znlevsoi + orig_scalez * ( exp( orig_zecoeff * (inactive-0.5) )
                                             - exp( orig_zecoeff * (nlevsoi -0.5) ) )
}#end if (nlevsoi < nlevgrnd)
# Indices for derived quantities
jtop     = pmax(1L      ,sequence(nlevgrnd)-1)
jbot     = pmin(nlevgrnd,sequence(nlevgrnd)+1)

# Soil layer thickness
dzsoi    = c( 0.5 * (zsoi[jbot[-nlevgrnd]] + zsoi[jtop[-nlevgrnd]])
            , zsoi[nlevgrnd]-zsoi[nlevgrnd-1]
            )#end c

# Soil layer interfaces
ztop     = c(0,0.5*(zsoi[-nlevgrnd]+zsoi[jbot[-nlevgrnd]]))
zbot     = c(ztop[-1L],zsoi[nlevgrnd]+0.5*dzsoi[nlevgrnd])

Domain file

The domain file is written entirely from the information provided at the beginning of this document.

First, we set the centre and edge of the grid cells. Make sure longitude is always set from \(0^\circ\) to \(360^\circ\), and that the centre of the latitude points are not too close to the poles.

# Find precision for the coordinates (typically 1% of the grid size).
cprec = -floor(log10(dxy*0.01))

# Standardise coordinates (and make sure latitude cannot exceed the poles).
clon  = round(site_lon,cprec) %% 360.
clat  = max(-90,min(90-0.5*dxy,round(site_lat,cprec)))

# Find edges
wlon = (clon - 0.5 * dxy) %% 360.
elon = (clon + 0.5 * dxy) %% 360.
slat = max(-90.,clat - 0.5 * dxy)
nlat = min(+90.,clat + 0.5 * dxy)

Find the area of this grid cell, in steradians, using the definition of solid angle \(\omega\): \[\begin{align} & \omega = \left[\sin{\left(\varphi_{N}\right)} - \sin{\left(\varphi_{S}\right)} \right] \, \left[ \left(\lambda_{E} - \lambda_{W} \right) \mod{2\,\pi{}} \right] \end{align}\] where \(\varphi_{S}\) and \(\varphi_{N}\) are the latitudes of the southern and northern edges [\(\mathrm{rad}\)], and \(\lambda_{W}\) and \(\lambda_{E}\) are the longitude of the western and eastern edges [\(\mathrm{rad}\)]. The modulo operator ensures that the results are correct when the centre of the grid cell crosses the Greenwich meridian.

# Turn angles to radians.
wlonr = wlon * pi / 180.
elonr = elon * pi / 180.
slatr = slat * pi / 180.
nlatr = nlat * pi / 180.

# Find solid angle area (assuming radius=1 so results are in steradians).
solid_angle = (sin(nlatr)-sin(slatr)) * ((elonr-wlonr) %% (2*pi))

Define the dimensions for the new file. It should be always a single point:

# Dimensions are unitless
ni = ncdim_def( name="ni",units="",vals=sequence(1L),create_dimvar=FALSE)
nj = ncdim_def( name="nj",units="",vals=sequence(1L),create_dimvar=FALSE)
nv = ncdim_def( name="nv",units="",vals=sequence(4L),create_dimvar=FALSE)

# List of dimensions
ij  = list(ni,nj)
vij = list(nv,ni,nj)

Create a list with the variables to be added to the single-point domain file. Each list element contains the following variables.

  • vals. The values to be included in the variables.
  • long. The variable long (and descriptive) name.
  • unit. The variable units. In case the variable does not have units, leave it blank ("").
  • dims. The variable dimensions (a list with the dimensions defined above).
  • attx. Other relevant information to be added as attributes. In case no additional attribute is needed, set this to list()
# List of data to write.
nco_dat      = list()
nco_dat$xc   = list( vals = array(data = clon                  , dim =   c(1,1))
                   , long = "longitude of the grid cell centre"
                   , unit = "degrees_east"
                   , dims = ij
                   , prec = "double"
                   , attx = list( bounds = "xv")
                   )#end list
nco_dat$yc   = list( vals = array(data = clat                  , dim =   c(1,1))
                   , long = "latitude of the grid cell centre"
                   , unit = "degrees_north"
                   , dims = ij
                   , prec = "double"
                   , attx = list( bounds = "yv")
                   )#end list
nco_dat$xv   = list( vals = array(data = c(wlon,elon,elon,wlon), dim = c(4,1,1))
                   , long = "longitude of grid cell vertices"
                   , unit = "degrees_east"
                   , dims = vij
                   , prec = "double"
                   , attx = list(note = "Vertex order: 1 - SW; 2 - SE; 3 - NE; 4 - NW")
                   )#end list
nco_dat$yv   = list( vals = array(data = c(slat,slat,nlat,nlat), dim = c(4,1,1))
                   , long = "latitude of grid cell vertices"
                   , unit = "degrees_north"
                   , dims = vij
                   , prec = "double"
                   , attx = list(note = "Vertex order: 1 - SW; 2 - SE; 3 - NE; 4 - NW")
                   )#end list
nco_dat$mask = list( vals = array(data =          1L           , dim =   c(1,1))
                   , long = "land domain mask"
                   , unit = ""
                   , dims = ij
                   , prec = "integer"
                   , attx = list( note        = "Unitless."
                                , coordinates = "xc yc"
                                , classes     = "0 - water (not active); 1 - land (active)"
                                )#end list
                   )#end list
nco_dat$frac = list( vals = array(data =          1.           , dim =   c(1,1))
                   , long = "fraction of grid cell that is active"
                   , unit = ""
                   , dims = ij
                   , prec = "double"
                   , attx = list( note        = "Unitless."
                                , coordinates = "xc yc"
                                , filter1     = paste0( "error if frac> 1.0+eps or "
                                                      , "frac < 0.0-eps; eps = 0.1000000E-11"
                                                      )#end paste0
                                , filter2     = paste0( "limit frac to [fminval,fmaxval];"
                                                      , "fminval= 0.1000000E-02 fmaxval=  1.000000"
                                                      )#end paste0
                                )#end list
                   )#end list
nco_dat$area = list( vals = array(data = solid_angle           , dim =   c(1,1))
                   , long = "area (solid angle) of grid cell, in steradian"
                   , unit = "sr"
                   , dims = ij
                   , prec = "double"
                   , attx = list( coordinates = "xc yc")
                   )#end list

Define the netCDF variables for the output variables.

nco_vlist = list()
for (v in seq_along(nco_dat)){
   # Load information for this variable.
   v_vnow   = names(nco_dat)[v]       # Variable name
   v_long   = nco_dat[[v_vnow]]$long  # Variable description ("long name")
   v_unit   = nco_dat[[v_vnow]]$unit  # Units
   v_dims   = nco_dat[[v_vnow]]$dims  # Variable dimensions
   v_prec   = nco_dat[[v_vnow]]$prec  # Variable type

   # Define netCDF variable
   nco_vlist[[v_vnow]] = ncvar_def( name     = v_vnow
                                  , units    = v_unit
                                  , dim      = v_dims
                                  , longname = v_long
                                  , prec     = v_prec
                                  )#end ncvar_def
}#end for (v in seq_along(nci_vlist))

Create the output netCDF file.

# Create the netCDF connection
if ("nc_domaout" %in% ls()){ dummy=nc_close(nc_domaout); file.remove(output_domain); rm(nc_domaout)}
nc_domaout = nc_create(filename=output_domain,vars=nco_vlist)

# Define the code developer information (indirect way so the email is not visible).
developer_name  = c( 111L, 103L, 110L, 111L,  76L,  32L, 115L, 111L,  99L, 114L,  97L,  77L)
developer_email = c( 118L, 111L, 103L,  46L, 108L,  98L, 108L,  64L, 111L, 103L, 110L, 111L
                   , 108L, 109L)

# Generate information about this file.
glob_att = list( title           = paste0( "Domain file for ",site_desc,".")
               , version         = dat_version
               , date_created    = paste0(as.character(now(tzone="UTC"))," UTC")
               , source_code     = "make_fates_domain+surface.Rmd"
               , code_notes      = "Domain file compatible with ELM-FATES and CLM-FATES"
               , code_developer  = paste0( intToUtf8(rev(developer_name))
                                         ," <"
                                         , intToUtf8(rev(developer_email))
                                         ,">"
                                         )#end paste0
               , file_author     = paste0(author_name," <",author_email,">")
               )#end list

# Add information to the global attribute list
for (l in seq_along(glob_att)){
   # Current attribute information
   att_name  = names(glob_att)[l]
   att_value = glob_att[[l]]

   # Add attribute 
   dummy = ncatt_put(nc=nc_domaout,varid=0,attname=att_name,attval=att_value)
}#end for (l in seq_along(global_att))

Populate the netCDF with the domain variables, and include the attributes for each variable.

for (v in seq_along(nco_dat)){
   # Retrieve variable information
   v_vnow  = names(nco_dat)[v]
   nco_now = nco_dat[[v_vnow]]

   # Add variable values
   dummy = ncvar_put(nc = nc_domaout,varid=v_vnow,vals=nco_now$vals)

   # Add extra attributes
   for (a in seq_along(nco_now$attx)){
      # Retrieve attribute information
      a_name  = names(nco_now$attx)[a]
      a_value = nco_now$attx[[a]] 
      
      dummy = ncatt_put(nc = nc_domaout, varid=v_vnow,attname=a_name,attval=a_value)
   }#end for (a in seq_along(v_attx))
}#end for (v in seq_along(nco_vlist))

Save new netCDF, and close the output file. We remove the connection from memory.

   dummy = nc_close(nc_domaout)
   rm(nc_domaout)

Surface data file

To generate the grid surface data file, we will load an existing surface data file, locate the grid cell that is the closest to the point of interest, abd extract the data. For some of the variables, such as soil texture and soil organic carbon, there may be local observations. If this is the case, we replace the gridded information with the local observations.

Load the gridded surface data

Open the surface data file, and retrieve the grid dimensions.

# Open gridded surface data
if ("nc_gridin" %in% ls()){dummy=nc_close(nc_gridin);rm(nc_gridin)}
nc_gridin = nc_open(filename=input_surfgrid)

Retrieve global attributes from the input file, and add information about this processing.

# Retrieve original attributes. We only make minor edits.
glob_att = ncatt_get(nc = nc_gridin, varid = 0)


# Define the code developer information (indirect way so the email is not visible).
developer_name  = c( 111L, 103L, 110L, 111L,  76L,  32L, 115L, 111L,  99L, 114L,  97L,  77L)
developer_email = c( 118L, 111L, 103L,  46L, 108L,  98L, 108L,  64L, 111L, 103L, 110L, 111L
                   , 108L, 109L)

# Get current time for the log
create_time = paste0(as.character(now(tzone="UTC"))," UTC")

# Generate information about this file.
glob_new = list( title           = paste0( "Domain file for ",site_desc,".")
               , Version         = dat_version
               , Revision_Id     = paste0( "$Id: make_fates_domain+surface.Rmd"
                                         , " 0 ",create_time,Sys.info()["user"],"$"
                                         )#end paste0
               , History_Log     = paste0( "created on: ", create_time)
               , Logname         = paste0( "code originally developed by "
                                         , intToUtf8(rev(developer_name))
                                         ," <"
                                         , intToUtf8(rev(developer_email))
                                         ,">"
                                         )#end paste0
               , Host            = Sys.info()["nodename"]
               , File_Author     = paste0(author_name," <",author_email,">")
               )#end list

# Update attribute list
glob_att = modifyList( glob_att, glob_new)

Load coordinates, and identify the nearest grid point.

# Retrieve longitude and latitude
in_lon   = melt(ncvar_get(nc=nc_gridin,"LONGXY"      )); names(in_lon)  = c("x","y","lon"   )
in_lat   = melt(ncvar_get(nc=nc_gridin,"LATIXY"      )); names(in_lat)  = c("x","y","lat"   )
in_veg   = melt(ncvar_get(nc=nc_gridin,"PCT_NATVEG"  )); names(in_veg)  = c("x","y","pcveg" )
in_land  = melt(ncvar_get(nc=nc_gridin,"LANDFRAC_PFT")); names(in_land) = c("x","y","fland" )

# Create a tibble with coordinates
in_coord = tibble(merge(merge(merge(in_lon,in_lat),in_veg),in_land)) 


# Define function that takes longitude/latitude and find distances.
rdist_wrap = function(lon,lat,lon0,lat0){
   ans = c(fields::rdist.earth(x1=cbind(lon0,lat0),x2=cbind(lon,lat),miles=FALSE))
   return(ans)
}#end function

# Find the distances.
in_coord = in_coord %>%
           mutate( dist = rdist_wrap(lon=lon,lat=lat,lon0=clon,lat0=clat))


# Select the nearest neighbour
in_near = in_coord %>% 
          filter((pcveg > pcveg_min) & (fland > landfrac_min)) %>% 
          slice(which.min(dist))
in_xy   = in_near %>% select(c(x,y)) %>% unlist(); names(in_xy) = NULL

Load all the dimensions from the input netCDF file, and create dimensions for the output file that are essentially copies of the input. The only exceptions are the longitude and latitude, which will be always one (single site).

# List of dimension names
nc_dnames = names(nc_gridin$dim)

# Loop through all the dimensions
nc_dims = list()
for (d in seq_along(nc_dnames)){
   # Retrieve dimensions
   d_dname   = nc_dnames[d]
   nc_dimnow = nc_gridin$dim[[d_dname]]

   # Fix dimensions and values for longitude and latitude.
   if (d_dname %in% c("lsmlon","lsmlat")){
      # Longitude or latitude, make data a single point
      nc_dimnow = modifyList( x   = nc_dimnow
                            , val = list( len = 1L, vals = sequence(1L))
                            )#end modifyList
   # }else if (d_dname %in% "time"){
   #    # Time is not properly set in some nc maps.
   #    nc_dimnow = modifyList( x   = nc_dimnow
   #                          , val = list(vals=seq_along(nc_dimnow$vals))
   #                          )#end modifyList
   }#end if (nc_dimnow)

   # Decide whether to append calendar information
   if (nc_dimnow$unlim && (! is.null(nc_dimnow$calendar))){
      nc_calendar = nc_dimnow$calendar
   }else{
      nc_calendar = NA_character_
   }#end if (nc_dimnow$unlim && (! is.null(nc_dimnow$calendar)))

      
   # Create dimensions. Distinguish unlimited and limited dimensions (if needed).
   nc_dims[[d_dname]] = ncdim_def( name          = d_dname
                                 , units         = nc_dimnow$units
                                 , vals          = nc_dimnow$vals
                                 , unlim         = nc_dimnow$unlim
                                 , create_dimvar = nc_dimnow$create_dimvar
                                 , calendar      = nc_calendar
                                 )#end ncdim_def
}#end for (d in seq_along(nc_dnames))

In case depth to bedrock in known, include it in the list of variables. We also reset the number of active soil layers nlevsoi and adjust the variables that depend upon nlevsoi

#   Check whether or not to overwrite the number of active layers
use_depth_geotiff  = tolower(site_bedrock) %in% "gridded"
use_depth_constant = (! use_depth_geotiff) && (! is.na(site_bedrock))

if (use_depth_geotiff || use_depth_constant){
   # Check how to define the depth to bedrock
   if (use_depth_constant){
      # Depth to bedrock is from user-defined constant
      ud_bedrock = br_scale * site_bedrock
   }else{

      # Obtain depth to bedrock from the GeoTIFF file
      rast_bedrock = rast(x=input_soildepth)
      
      #    Append the coordinates into a spatial vector
      site_coord = data.frame(lon = site_lon, lat = site_lat)
      site_coord = vect(x = site_coord, geom = c("lon","lat"), crs = "epsg:4326")

      # Decide how to extract the sample
      if (is.na(br_window)){
         # Reproject coordinates to the same projection used by the raster
         site_coord = project(x = site_coord, y = crs(rast_bedrock))
         
         # No window, simply extract the nearest pixel
         br_neighbour = terra::extract(x=rast_bedrock, y = site_coord)[,2L]
         
      }else{
         # Add a buffer so we extract multiple pixels
         site_buffer = buffer(x=site_coord,width=br_window)

         # Reproject the coordinates so they match the raster.
         site_coord  = project(x = site_coord , y = crs(rast_bedrock))
         site_buffer = project(x = site_buffer, y = crs(rast_bedrock))
         
         # Extract the pixels within the buffer
         br_neighbour = terra::extract(x=rast_bedrock, y = site_buffer)[,2L]
      }#end if (! is.na(br_window))

      # Average the sampled points, and fix units
      ud_bedrock = br_scale * mean(br_neighbour,na.rm = TRUE)
   }#end if (is.na(br_direction))


   # Find the layer associated with the depth to bedrock
   nlevsoi            = max(which(ud_bedrock >= ztop))
   nlevsoi_orig       = nc_dims$nlevsoi$len

   # Overwrite dimensions for nlevsoi
   nc_dims$nlevsoi = ncdim_def( name          = nc_dims$nlevsoi$name
                              , units         = nc_dims$nlevsoi$units
                              , vals          = sequence(nlevsoi)
                              , unlim         = nc_dims$nlevsoi$unlim
                              , create_dimvar = nc_dims$nlevsoi$create_dimvar
                              , calendar      = nc_calendar
                              )#end ncdim_def

      
   # Find the sequence of indices for updating variables that depend on the soil
   idx_levsoi = pmin(nlevsoi_orig,sequence(nlevsoi))


   # Append information for depth to bedrock
   if (use_depth_geotiff){
      map_soil_depth_file = basename(input_soildepth)
   }else{
      map_soil_depth_file = "site-specific information"
   }#end if (use_soilgrids)
   glob_att = modifyList( x   = glob_att
                        , val = list( map_soil_depth_file = map_soil_depth_file )
                        )#end modifyList
}else{
   # Save the number of active soil layers in case we need for soil interpolation
   nlevsoi_orig = nc_dims$nlevsoi$len
   idx_levsoi   = sequence(nlevsoi)
}#end if (use_geotiff || use_constant)

Define variables that should feature in the output. Every variable in the input should be in the output too.

# List of variable names
nc_vnames = names(nc_gridin$var)

# This will contain variables that were actually used. We will create dummy variables for 
# those not used, to ensure they exist in the output.
nc_dimused = character(0)

# Loop through all the variables, retrieve data and settings
nc_vars    = list()
nc_values  = list()
for (v in seq_along(nc_vnames)){
   # Retrieve variable information
   v_vname = nc_vnames[v]
   v_vinfo = nc_gridin$var [[v_vname]]
   v_vlong = v_vinfo$longname
   v_vunit = v_vinfo$unit
   v_vprec = if( v_vinfo$prec %in% "int"){"integer"}else{v_vinfo$prec}
   v_vmiss = if( v_vinfo$prec %in% c("float","double")){v_vinfo$missval}else{NULL}

   # Switch the longitude and latitude dimensions with point
   if (is.null(v_vinfo$varsize)){
      # Dummy dimensions
      v_count = NA
      v_start = NA
      v_vdims = list()
   }else{
      # Load count, and assume initially that all dimensions start at 1.
      v_count = v_vinfo$varsize
      v_start = rep(1L,times=length(v_count))

      # Find dimensions 
      v_dname = c(unlist(do.call(rbind,v_vinfo$dim)[,"name"]))
      v_vdims = nc_dims[v_dname]
      
      # Find longitude and latitude dimensions
      ixy  = match(c("lsmlon","lsmlat"),v_dname)
      sel  = ! is.na(ixy)

      # Make sure we only read the data from the nearest neighbour
      v_count[ixy[sel]]   = c(1,1)[sel]
      v_start[ixy[sel]]   = in_xy [sel]
      names(v_start) = NULL
   }#end if (length(v_start) == 0)

   # Load information to define the data set.
   nc_vars[[v_vname]] = ncvar_def( name     = v_vname
                                 , units    = v_vunit
                                 , dim      = v_vdims
                                 , missval  = v_vmiss
                                 , longname = v_vlong
                                 , prec     = v_vprec
                                 )#end ncvar_def

   #    Load the data to a temporary array. We may need to adjust it if the soil depth 
   # dimension was updated.
   nc_datum = ncvar_get( nc             = nc_gridin
                       , varid          = v_vname
                       , start          = v_start
                       , count          = v_count
                       , collapse_degen = FALSE
                       )#end ncvar_get

   # Update the data with the revised number of dimensions if this is a soil variable.
   if ("nlevsoi" %in% names(nc_vars[[v_vname]]$dim)){
      nc_values[[v_vname]] = nc_datum[,,idx_levsoi,drop=FALSE]
   }else{
      nc_values[[v_vname]] = nc_datum
   }#end if ("nlevsoi" %in% names(nc_vars[[v_vname]]$dim))

   
   # Update the list of dimensions used.
   nc_dimused = sort(unique(c(nc_dimused,names(v_vdims))))
}#end for (v in seq_along(nc_vnames))

Append soil depth.

# Append soil layer information to surface data.
nc_vars$ZSOI  = ncvar_def( name     = "ZSOI"
                         , units    = "m"
                         , dim      = nc_dims[c("nlevsoi")]
                         , missval  = nc_gridin$var$PCT_SAND$missval
                         , longname = "mid-layer soil depth. Note: in ELM, this will overwrite the default soil depth."
                         )#end ncvar_def 

# Append value to the list of values
nc_values$ZSOI  = zsoi

If soil depth was provided, make sure that the value is appended to the output. This must be done after loading the other variables and updating the dimensions. The default name in CLM is zbedrock, whilst ELM uses aveDTB. To ensure that the surface file is is agnostic to the host land model, we add it twice. If ELM accepts the pull request for cross-model compatibility, we can keep one of these defaults only.

if (use_depth_geotiff || use_depth_constant){
   # Append depth to bedrock to surface data.
   nc_vars$zbedrock = ncvar_def( name     = "zbedrock"
                               , units    = "m"
                               , dim      = nc_dims[c("lsmlon","lsmlat")]
                               , missval  = nc_gridin$var$PCT_SAND$missval
                               , longname = "soil depth"
                               )#end ncvar_def 
   nc_vars$aveDTB   = ncvar_def( name     = "aveDTB"
                               , units    = "m"
                               , dim      = nc_dims[c("lsmlon","lsmlat")]
                               , missval  = nc_gridin$var$PCT_SAND$missval
                               , longname = "soil depth"
                               )#end ncvar_def 

   # Append value to the list of values
   nc_values$zbedrock = array(data=ud_bedrock,dim=c(1L,1L))
   nc_values$aveDTB   = array(data=ud_bedrock,dim=c(1L,1L))
}#end if (use_depth_geotiff || use_depth_constant)

Create dummy variables for unused dimensions. It may not be necessary, but this ensures every variable is included.

# Make data set
is_unused = which(! (names(nc_dims) %in% nc_dimused))
for (u in is_unused){
   # Retrieve dimension information
   u_dname = names(nc_dims)[u]
   un_dims = nc_dims[[u_dname]]
   u_dlong = paste0("Dummy variable to retain dimension ",un_dims$longname)
   u_dunit = un_dims$units
   u_dvals = un_dims$vals
   u_dprec = typeof(u_dvals)

   # Load information to define the data set.
   nc_vars[[u_dname]] = ncvar_def( name     = u_dname
                                 , units    = u_dunit
                                 , dim      = un_dims
                                 , longname = u_dlong
                                 , prec     = u_dprec
                                 )#end ncvar_def

   # Load the data 
   nc_values[[u_dname]] = u_dvals
   
}#end for (u in is_unused)

In case force_natural is true, we impose percentage of natural vegetation to 100%.

# Re-set coordinates
nc_values$LONGXY = 0. * nc_values$LONGXY + clon
nc_values$LATIXY = 0. * nc_values$LATIXY + clat

# Re-define percentage of natural vegetation to 100%, and other classes to 0%
if (force_natural){
   nc_values$PCT_NATVEG  = 0. * nc_values$PCT_NATVEG + 100.
   nc_values$PCT_URBAN   = 0. * nc_values$PCT_URBAN
   nc_values$PCT_CROP    = 0. * nc_values$PCT_CROP
   nc_values$PCT_WETLAND = 0. * nc_values$PCT_WETLAND
   nc_values$PCT_LAKE    = 0. * nc_values$PCT_LAKE
   nc_values$PCT_GLACIER = 0. * nc_values$PCT_GLACIER
}#end if (force_natural)

In case texture and soil organic content were provided, we use the site-specific information instead. We test whether the user provided the information directly or if we are supposed to retrieve it from SoilGrids.

# In case texture was defined, set the values.
use_soilgrids = any(tolower(c(site_xsand,site_xclay)) %in% "soilgrids")
use_site      = (! use_soilgrids) && all(! is.na(c(site_xsand,site_xclay)))
if ( use_soilgrids || use_site){
   
   # Check how to retrieve the soil layers.
   if (use_soilgrids){
      # Retrieve soil texture from SoilGrids
      ud_texture = fetchSoilGrids( x         = data.frame(id=1,lat=site_lat,lon=site_lon)
                                 , variables = c("sand","clay")
                                 )#end fetchSoilGrids

      # Save variables to vectors.
      n_ud_layer = length(ud_texture$id)
      ud_ztop    = 0.01 * ud_texture$hzdept
      ud_zbot    = 0.01 * ud_texture$hzdepb
      ud_xsand   = ud_texture$sandmean
      ud_xclay   = ud_texture$claymean
      
   }else{
      #    Retrieve soil texture from user input. 

      #    Define soil layers from user input. If the user did not provide user layers,
      # assume the layers to be the same as the default soil grid in the host land models.
      if (is.na(site_zbot)){
         # Soil depths not specified. Assume the same as the host land model.
         ud_ztop = ztop[sequence(nlevsoi)]
         ud_zbot = zbot[sequence(nlevsoi)]
      }else{
         #    Soil depths provided. Define the top of each layer, assuming top depth = 0 for
         # the topmost layer.
         ud_ztop = c(0,site_zbot[-length(site_zbot)])
         ud_zbot = site_zbot
      }#end if (is.na(site_zbot))

      # Number of user-defined layer
      n_ud_layer = length(ud_ztop)

      #    Make sure that the length of user-defined textures and soil layers match, or 
      # that the user provided a single value to be used everywhere.
      if (length(site_xsand) == 1L ){
         ud_xsand = rep(site_xsand,times=n_ud_layer)
      }else{
         ud_xsand = site_xsand
      }#end if (length(site_xsand) == 1L )
      if (length(site_xclay) == 1L ){
         ud_xclay = rep(site_xclay,times=n_ud_layer)
      }else{
         ud_xclay = site_xclay
      }#end if (length(site_xclay) == 1L )

      #    Make sure the lengths of the input soil textures are all consistent with the 
      # number of input soil layers.
      if (any(c(length(ud_xsand),length(ud_xclay)) != n_ud_layer)){
         cat("---~---\n")
         cat(" - Length of input sand texture: ",length(site_xsand),".\n",sep="")
         cat(" - Length of input clay texture: ",length(site_xclay),".\n",sep="")
         cat(" - Number of soil layers: "       ,length(n_ud_layer),".\n",sep="")
         cat("    Soil texture must be either a scalar or a vector that matches\n")
         cat(" the number of layers.\n")
         cat("---~---\n")
         stop(" Mismatch between input texture and soil layers.")
      }#end if (any(c(length(ud_xsand),length(ud_xclay)) != n_ud_layer))
   }# end if (use_soilgrids)

      
   # Keep only layers that are within the model grid.
   ud_keep    = ud_ztop < zbot[nlevsoi]
   ud_ztop    = ud_ztop [ud_keep]
   ud_zbot    = ud_zbot [ud_keep]
   ud_xsand   = ud_xsand[ud_keep]
   ud_xclay   = ud_xclay[ud_keep]
   
   #    We carry over the information of the deepest SoilGrids layer all the way to the 
   # bottom of the host land model grid
   ud_zbot[n_ud_layer] = zbot[nlevsoi]

   #    Loop through the surface file soil layers and interpolate texture.
   for (z in sequence(nlevsoi)){
      #    For the interpolation, we find how much of the input layer overlaps with the
      # host land model layer. We use the overlapping thickness of each input layer as 
      # weights for the host land model interpolated texture.
      ud_lyr_ztop  = ifelse( test = ud_zbot < ztop[z]
                           , yes  = ztop[z]
                           , no   = ifelse( test = ud_ztop > zbot[z]
                                          , yes  = zbot[z]
                                          , no   = pmax(ud_ztop,ztop[z])
                                          )#end ifelse
                           )#end ifelse
      ud_lyr_zbot  = ifelse( test = ud_zbot < ztop[z]
                           , yes  = ztop[z]
                           , no   = ifelse( test = ud_ztop > zbot[z]
                                          , yes  = zbot[z]
                                          , no   = pmin(ud_zbot,zbot[z])
                                          )#end ifelse
                           )#end ifelse
      ud_lyr_thick = ud_lyr_zbot - ud_lyr_ztop

      # Find the interpolated texture.
      ud_lyr_xsand = weighted.mean(x=ud_xsand,w=ud_lyr_thick)
      ud_lyr_xclay = weighted.mean(x=ud_xclay,w=ud_lyr_thick)
      ud_lyr_xsilt = 100. - ud_lyr_xsand - ud_lyr_xclay

      # Set a vector with textures to test whether or not to use site-specific data
      ud_lyr_xtext = c(ud_lyr_xsand,ud_lyr_xsilt,ud_lyr_xclay)

      # If the interpolated texture is valid, update the values
      if (all( (ud_lyr_xtext >= 0.) & (ud_lyr_xtext <= 100.) ) ){
         nc_values$PCT_SAND[,,z] = ud_lyr_xsand
         nc_values$PCT_CLAY[,,z] = ud_lyr_xclay
      }#end if (all( (ud_lyr_xtext >= 0.) & (ud_lyr_xtext <= 100.) ) )
   }#end for (z in sequence(nlevsoi))

   # Update global attribute list to inform that the data are site-specific
   if (use_soilgrids){
      map_soil_texture_file = "SoilGrids 2.0 (obtained for the site through R package soilDB)"
   }else{
      map_soil_texture_file = "site-specific information"
   }#end if (use_soilgrids)
   glob_att = modifyList( x   = glob_att
                        , val = list( map_soil_texture_file = map_soil_texture_file )
                        )#end modifyList
}#end if (use_soilgrids || use_site)

In case texture and soil organic content were provided, we use the site-specific information instead. We test whether the user provided the information directly or if we are supposed to retrieve it from SoilGrids.

# In case texture was defined, set the values.
use_soilgrids = tolower(site_organic) %in% "soilgrids"
use_site      = ( ! use_soilgrids ) && (! is.na(site_organic) )
if ( use_soilgrids || use_site){
   
   # Check how to retrieve the soil layers.
   if (use_soilgrids){
      # Retrieve soil texture from SoilGrids
      ud_extract = fetchSoilGrids( x         = data.frame(id=1,lat=site_lat,lon=site_lon)
                                 , variables = "ocd"
                                 )#end fetchSoilGrids

      # Retrieve the variables
      n_ud_layer = length(ud_extract$id)
      ud_ztop    = 0.01 * ud_extract$hzdept
      ud_zbot    = 0.01 * ud_extract$hzdepb
      ud_organic = ud_extract$ocdmean / 0.58 # To convert it to kg organic matter

   }else{
      #    Retrieve soil organic matter density from user input. 

      #    Define soil layers from user input. If the user did not provide user layers,
      # assume the layers to be the same as the default soil grid in the host land models.
      if (is.na(site_zbot)){
         # Soil depths not specified. Assume the same as the host land model.
         ud_ztop = ztop[sequence(nlevsoi)]
         ud_zbot = zbot[sequence(nlevsoi)]
      }else{
         #    Soil depths provided. Define the top of each layer, assuming top depth = 0 for
         # the topmost layer.
         ud_ztop = c(0,site_zbot[-length(site_zbot)])
         ud_zbot = site_zbot
      }#end if (is.na(site_zbot))

      # Number of user-defined layer
      n_ud_layer = length(ud_ztop)

      # If the user provided constant soil organic matter density, turn it into a vector.
      if (length(site_organic) == 1L ){
         ud_organic = rep(site_organic,times=n_ud_layer)
      }else{
         ud_organic = site_organic
      }#end if (length(site_organic) == 1L )

      #    Make sure the length of the input data is consistent with the number of input 
      # soil layers.
      if (length(ud_organic) != n_ud_layer){
         cat("---~---\n")
         cat(" - Length of input organic matter: ",length(site_organic),".\n",sep="")
         cat(" - Number of soil layers: "         ,length(n_ud_layer)  ,".\n",sep="")
         cat("    Site organic matter density must be either a scalar or a vector\n")
         cat(" that matches the number of layers.\n")
         cat("---~---\n")
         stop(" Mismatch between input texture and soil layers.")
      }#end if (length(ud_organic) != n_ud_layer)
   }# end if (use_soilgrids)

      
   # Keep only layers that are within the model grid.
   ud_keep    = ud_ztop < zbot[nlevsoi]
   ud_ztop    = ud_ztop [ud_keep]
   ud_zbot    = ud_zbot [ud_keep]
   ud_xsand   = ud_xsand[ud_keep]
   ud_xclay   = ud_xclay[ud_keep]
   
   #    We carry over the information of the deepest SoilGrids layer all the way to the 
   # bottom of the host land model grid
   ud_zbot[n_ud_layer] = zbot[nlevsoi]

   #    Loop through the surface file soil layers and interpolate soil organic matter 
   # density.
   for (z in sequence(nlevsoi)){
      #    For the interpolation, we find how much of the input layer overlaps with the
      # host land model layer. We use the overlapping thickness of each input layer as 
      # weights for the host land model interpolated texture.
      ud_lyr_ztop  = ifelse( test = ud_zbot < ztop[z]
                           , yes  = ztop[z]
                           , no   = ifelse( test = ud_ztop > zbot[z]
                                          , yes  = zbot[z]
                                          , no   = pmax(ud_ztop,ztop[z])
                                          )#end ifelse
                           )#end ifelse
      ud_lyr_zbot  = ifelse( test = ud_zbot < ztop[z]
                           , yes  = ztop[z]
                           , no   = ifelse( test = ud_ztop > zbot[z]
                                          , yes  = zbot[z]
                                          , no   = pmin(ud_zbot,zbot[z])
                                          )#end ifelse
                           )#end ifelse
      ud_lyr_thick = ud_lyr_zbot - ud_lyr_ztop

      #    Find the interpolated soil organic matter density.
      nc_values$ORGANIC[,,z] = weighted.mean(x=ud_organic,w=ud_lyr_thick)
   }#end for (z in sequence(nlevsoi))

   # Update global attribute list to inform that the data are site-specific
   if (use_soilgrids){
      map_soil_organic_file = "SoilGrids 2.0 (obtained for the site through R package soilDB)"
   }else{
      map_soil_organic_file = "site-specific information"
   }#end if (use_soilgrids)
   glob_att = modifyList( x   = glob_att
                        , val = list( map_soil_organic_file = map_soil_organic_file )
                        )#end modifyList
}#end if (use_soilgrids || use_site)

Variable T_BUILDING_MAX used to be part of every surface data. Apparently CLM dropped this variable, but ELM continues to require it. If the reference surface data set is a recent one, this variable will be missing and the surface data file will not work with ELM. To address this, we use T_BUILDING_MIN to re-generate T_BUILDING_MAX, by applying an offset compatible with older surface data files.

if (! ("T_BUILDING_MAX" %in% names(nc_vars))){
   # Create maximum interior building temperature
   nc_vars$T_BUILDING_MAX = ncvar_def( name     = "T_BUILDING_MAX"
                                     , units    = "K"
                                     , dim      = nc_dims[c("lsmlon","lsmlat","numurbl")]
                                     , missval  = nc_gridin$var$T_BUILDING_MIN$missval
                                     , longname = "maximum interior building temperature"
                                     )#end ncvar_def 

   # Append value to the list of values
   dT_building              = c(15,rep(39,times=nc_dims$numurbl$len-1))
   nc_values$T_BUILDING_MAX = nc_values$T_BUILDING_MIN + dT_building
}# end if (! ("T_BUILDING_MAX" %in% names(nc_vars)))

Create the output files.

# Create output netCDF
nc_surfout = nc_create(filename = output_surfdata,vars=nc_vars)

# Add global attributes
for (a in seq_along(glob_att)){
   # Retrieve attribute information
   a_name  = names(glob_att)[a]
   a_value = glob_att[[a_name]] 

   # Include global attribute      
   dummy = ncatt_put(nc = nc_surfout, varid=0,attname=a_name,attval=a_value)
}#end for (a in seq_along(glob_att))

# Add variables
for (v in seq_along(nc_values)){
   # Retrieve variable information
   v_name  = names(nc_values)[v]
   v_value = nc_values[[v_name]] 

   # Add variable values
   dummy = ncvar_put(nc = nc_surfout,varid=v_name,vals=v_value)
}#end for (v in seq_along(nc_values))

# Close input and output files
dummy = nc_close(nc = nc_gridin )
dummy = nc_close(nc = nc_surfout)