Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions models/sipnet/NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# PEcAn.SIPNET 1.10.0.9000

* Fixed a unit error in model2netcdf.SIPNET's calculation of `AGBI` (kgC/m2/sec)
from `woodCreation` (actually gC/m2/timestep, was being treated as gC/m2/day).
* `model2netcdf.SIPNET` now takes NPP directly from sipnet.out rather than repeat
Sipnet's internal calculation (as GPP - Ra) with identical results.
* Updated README with a more complete model description and instructions for installing SIPNET (#3705)
* Removed `tests/Rcheck_reference.log`, which was used to ignore historic check messages that have now been fixed.
* Initial support for SIPNET v2.0, whose features include simplified input files,
Expand Down
221 changes: 138 additions & 83 deletions models/sipnet/R/model2netcdf.SIPNET.R
Original file line number Diff line number Diff line change
Expand Up @@ -39,24 +39,25 @@ mergeNC <- function(
}

#--------------------------------------------------------------------------------------------------#
##' Convert SIPNET output to netCDF
##'
##' Converts all output contained in a folder to netCDF.
##'
##' @param outdir Location of SIPNET model output
##' @param sitelat Latitude of the site
##' @param sitelon Longitude of the site
##' @param start_date Start time of the simulation
##' @param end_date End time of the simulation
##' @param revision model revision
##' @param overwrite Flag for overwriting nc files or not
##' @param conflict Flag for dealing with conflicted nc files, if T we then will merge those, if F we will jump to the next.
##' @param prefix prefix to read the output files
##' @param delete.raw logical: remove sipnet.out files after converting?
##'
##' @export
##' @author Shawn Serbin, Michael Dietze
model2netcdf.SIPNET <- function(outdir, sitelat, sitelon, start_date, end_date, delete.raw = FALSE, revision, prefix = "sipnet.out",
#' Convert SIPNET output to netCDF
#'
#' Converts all output contained in a folder to netCDF.
#'
#' @param outdir Location of SIPNET model output
#' @param sitelat Latitude of the site
#' @param sitelon Longitude of the site
#' @param start_date Start time of the simulation
#' @param end_date End time of the simulation
#' @param revision model revision.
#' Ignored: PEcAn detects all relevant version differences from the format of the output file.
#' @param overwrite Flag for overwriting nc files or not
#' @param conflict Flag for dealing with conflicted nc files, if T we then will merge those, if F we will jump to the next.
#' @param prefix prefix to read the output files
#' @param delete.raw logical: remove sipnet.out files after converting?
#'
#' @export
#' @author Shawn Serbin, Michael Dietze
model2netcdf.SIPNET <- function(outdir, sitelat, sitelon, start_date, end_date, delete.raw = FALSE, revision = NULL, prefix = "sipnet.out",
overwrite = FALSE, conflict = FALSE) {
### Read in model output in SIPNET format
sipnet_out_file <- file.path(outdir, prefix)
Expand Down Expand Up @@ -118,7 +119,75 @@ model2netcdf.SIPNET <- function(outdir, sitelat, sitelon, start_date, end_date,


timestep.s <- 86400 / out_day



## Unit conversions
#
# CKB 20260407: Not using ud_convert here is intentional!
# This step is a consistent bottleneck to whole-run speed, and tests using
# ud_convert show a surprisingly large slowdown:
# In a test batch with ~500 rundirs run in parallel on a 2022-era SSD Macbook,
# the model stage took ~4.5x(!) longer with ud_convert than with simple scalars.
g_to_kg <- function(x) x / 1000
g_step_to_kg_sec <- function(x) x / 1000 / timestep.s
cm_to_mm <- function(x) x * 10
cm_step_to_mm_sec <- function(x) x * 10 / timestep.s
sipnet_output <- sipnet_output |>
dplyr::mutate(

# C and N pools
dplyr::across(
.cols = c(
# C pools are mandatory
dplyr::all_of(c("plantWoodC", "plantLeafC", "coarseRootC", "fineRootC", "soil", "litter")),
# N only present when turned on
dplyr::any_of(c("minN", "soilOrgN", "litterN"))
),
.fns = g_to_kg
),

# C and N fluxes
dplyr::across(
.cols = c(
dplyr::all_of(c("gpp", "nee", "npp", "rAboveground", "rRoot", "rtot", "rSoil")),
dplyr::any_of(c("woodCreation", "n2o", "nLeaching", "nFixation", "nUptake", "ch4"))
),
.fns = g_step_to_kg_sec
),

# Water pools
dplyr::across(
.cols = c(
dplyr::all_of(c("soilWater", "soilWetnessFrac", "snow")),
dplyr::any_of("litterWater") # Only present in V1 output
),
.fns = cm_to_mm
),

# Water fluxes
dplyr::across(
.cols = dplyr::all_of("evapotranspiration"),
.fns = cm_step_to_mm_sec
),
# Water flux special case:
# Sipnet reports transpiration, and no other variables, in cm/day not cm/timestep.
fluxestranspiration = cm_to_mm(.data$fluxestranspiration) / 86400, # cm/day -> mm/sec

# Date and time
datetime = sipnet2datetime(.data$year, .data$day, .data$time)
)


# calculate LAI for standard output
# LAI = plantLeafC / leafCSpWt
# both operands are in carbon units (gC/m2 and gC/m2_leaf),
# so no carbon fraction conversion (e.g. cFracLeaf) is needed.
param <- utils::read.table(file.path(gsub(pattern = "/out/",
replacement = "/run/", x = outdir),
"sipnet.param"), stringsAsFactors = FALSE)
leafCSpWt <- param[param[, 1] == "leafCSpWt", 2]
SLA <- 1000 / leafCSpWt # m2 leaf / kg C


### Loop over years in SIPNET output to create separate netCDF outputs
for (y in year_seq) {
Expand All @@ -133,18 +202,12 @@ model2netcdf.SIPNET <- function(outdir, sitelat, sitelon, start_date, end_date,
file.rename(file.path(outdir, paste(y, "nc", sep = ".")), file.path(outdir, "previous.nc"))
}
print(paste("---- Processing year: ", y)) # turn on for debugging

## Subset data for processing
sub.sipnet.output <- subset(sipnet_output, sipnet_output$year == y)

sub_dates <- sipnet2datetime(
y,
sub.sipnet.output[["day"]],
sub.sipnet.output[["time"]]
)

sub_dates_cf <- PEcAn.utils::datetime2cf(
sub_dates,
sub.sipnet.output$datetime,
paste0("days since ", y, "-01-01"),
tz = "UTC"
)
Expand All @@ -161,81 +224,71 @@ model2netcdf.SIPNET <- function(outdir, sitelat, sitelon, start_date, end_date,

## Setup outputs for netCDF file in appropriate units
output <- list(
"GPP" = PEcAn.utils::ud_convert(sub.sipnet.output$gpp, "g/m2", "kg/m2") / timestep.s,
"NPP" = PEcAn.utils::ud_convert(sub.sipnet.output$gpp - (sub.sipnet.output$rAboveground + sub.sipnet.output$rRoot), "g/m2", "kg/m2") / timestep.s,
"TotalResp" = PEcAn.utils::ud_convert(sub.sipnet.output$rtot, "g/m2", "kg/m2") / timestep.s,
"AutoResp" = (PEcAn.utils::ud_convert(sub.sipnet.output$rAboveground + sub.sipnet.output$rRoot, "g/m2", "kg/m2")) / timestep.s,
"HeteroResp" = PEcAn.utils::ud_convert(sub.sipnet.output$rSoil - sub.sipnet.output$rRoot, "g/m2", "kg/m2") / timestep.s,
"SoilResp" = PEcAn.utils::ud_convert(sub.sipnet.output$rSoil, "g/m2", "kg/m2") / timestep.s,
"NEE" = PEcAn.utils::ud_convert(sub.sipnet.output$nee, "g/m2", "kg/m2") / timestep.s,
"AbvGrndWood" = PEcAn.utils::ud_convert(sub.sipnet.output$plantWoodC, "g/m2", "kg/m2"),
"leaf_carbon_content" = PEcAn.utils::ud_convert(sub.sipnet.output$plantLeafC, "g/m2", "kg/m2"),
"TotLivBiom" = (PEcAn.utils::ud_convert(sub.sipnet.output$plantWoodC + sub.sipnet.output$plantLeafC +
sub.sipnet.output$coarseRootC + sub.sipnet.output$fineRootC, "g/m2", "kg/m2")),
"TotSoilCarb" = PEcAn.utils::ud_convert(sub.sipnet.output$soil + sub.sipnet.output$litter, "g/m2", "kg/m2")
"GPP" = sub.sipnet.output$gpp,
"NPP" = sub.sipnet.output$npp,
"TotalResp" = sub.sipnet.output$rtot,
"AutoResp" = sub.sipnet.output$rAboveground + sub.sipnet.output$rRoot,
"HeteroResp" = sub.sipnet.output$rSoil - sub.sipnet.output$rRoot,
"SoilResp" = sub.sipnet.output$rSoil,
"NEE" = sub.sipnet.output$nee,
"AbvGrndWood" = sub.sipnet.output$plantWoodC,
"leaf_carbon_content" = sub.sipnet.output$plantLeafC,
"litter_carbon_content" = sub.sipnet.output$litter,
"fine_root_carbon_content" = sub.sipnet.output$fineRootC,
"coarse_root_carbon_content" = sub.sipnet.output$coarseRootC,
"LAI" = sub.sipnet.output$plantLeafC * SLA,
"TotLivBiom" = sub.sipnet.output$plantWoodC + sub.sipnet.output$plantLeafC +
sub.sipnet.output$coarseRootC + sub.sipnet.output$fineRootC,
"TotSoilCarb" = sub.sipnet.output$soil + sub.sipnet.output$litter,
"AGB" = sub.sipnet.output$plantWoodC + sub.sipnet.output$plantLeafC,

# Water variables:
# Liquid water units are cm in Sipnet; in PEcAn they're kg water m-2
# (which is equivalent to mm: (water density = 1000 kg m-3) * (1 m/ 1000 mm) = (1 kg m-2)/mm
# Evapotranspiration in SIPNET is cm^3 water per cm^2 of area,
# already converted above to mm sec-1.
# To convert it to latent heat units W/m2 multiply by latent heat of vaporization (J kg-1)
# Latent heat of vaporization is not constant and it varies slightly with temperature, get.lv() returns 2.5e6 J kg-1 by default
"Qle" = sub.sipnet.output$evapotranspiration * PEcAn.data.atmosphere::get.lv(), # Qle W/m2/sec
"Transp" = sub.sipnet.output$fluxestranspiration,
"SoilMoist" = sub.sipnet.output$soilWater,
"SoilMoistFrac" = sub.sipnet.output$soilWetnessFrac,
"SWE" = sub.sipnet.output$snow # Snow Water Equivalent
)

# Water variables:
# Liquid water units are cm in Sipnet; in PEcAn they're kg water m-2
# (which is equivalent to mm, but ud_convert doesn't know that)
# Evapotranspiration in SIPNET is cm^3 water per cm^2 of area, to convert it to latent heat units W/m2 multiply with :
# 0.01 (cm2m) * 1000 (water density, kg m-3) * latent heat of vaporization (J kg-1)
# Latent heat of vaporization is not constant and it varies slightly with temperature, get.lv() returns 2.5e6 J kg-1 by default
output[["Qle"]] <- (PEcAn.utils::ud_convert(sub.sipnet.output$evapotranspiration, "cm", "mm") * PEcAn.data.atmosphere::get.lv()) / timestep.s # Qle W/m2
# Note that Sipnet reports transpiration, and no other variables, in cm/day not cm/timestep.
output[["Transp"]] <- PEcAn.utils::ud_convert(sub.sipnet.output$fluxestranspiration, "cm/day", "mm/sec") # Transpiration
output[["SoilMoist"]] <- PEcAn.utils::ud_convert(sub.sipnet.output$soilWater, "cm", "mm") # Soil moisture kgW/m2
output[["SoilMoistFrac"]] <- PEcAn.utils::ud_convert(sub.sipnet.output$soilWetnessFrac, "cm", "mm") # Fractional soil wetness
output[["SWE"]] <- PEcAn.utils::ud_convert(sub.sipnet.output$snow, "cm", "mm") # Snow Water Equivalent
output[["litter_carbon_content"]] <- PEcAn.utils::ud_convert(sub.sipnet.output$litter, "g/m2", "kg/m2")
# litterWater was removed in SIPNET v2; only extract if present
if ("litterWater" %in% names(sub.sipnet.output)) {
output[["litter_mass_content_of_water"]] <- PEcAn.utils::ud_convert(sub.sipnet.output$litterWater, "cm", "mm")
if ("litterWater" %in% names(sub.sipnet.output)) { # Removed in SIPNET v2; only extract if present
output[["litter_mass_content_of_water"]] <- sub.sipnet.output$litterWater
}

#calculate LAI for standard output
# LAI = plantLeafC / leafCSpWt
# both operands are in carbon units (gC/m2 and gC/m2_leaf),
# so no carbon fraction conversion (e.g. cFracLeaf) is needed.
param <- utils::read.table(file.path(gsub(pattern = "/out/",
replacement = "/run/", x = outdir),
"sipnet.param"), stringsAsFactors = FALSE)
leafCSpWt <- param[param[, 1] == "leafCSpWt", 2]
SLA <- 1000 / leafCSpWt # m2 leaf / kg C
output[["LAI"]] <- output[["leaf_carbon_content"]] * SLA
output[["fine_root_carbon_content"]] <- PEcAn.utils::ud_convert(sub.sipnet.output$fineRootC, "g/m2", "kg/m2")
output[["coarse_root_carbon_content"]] <- PEcAn.utils::ud_convert(sub.sipnet.output$coarseRootC, "g/m2", "kg/m2")
if ("woodCreation" %in% names(sub.sipnet.output)) {
output[["GWBI"]] <- PEcAn.utils::ud_convert(sub.sipnet.output$woodCreation, "g/m2/day", "kg/m2/s")
if ("woodCreation" %in% names(sub.sipnet.output)) { # Added in SIPNET v2; only extract if present
output[["GWBI"]] <- sub.sipnet.output$woodCreation
}
output[["AGB"]] <- PEcAn.utils::ud_convert(sub.sipnet.output$plantWoodC + sub.sipnet.output$plantLeafC, "g/m2", "kg/m2")

# columns only present in sipnet >= v2 with N and methane turned on
if ("minN" %in% names(sub.sipnet.output)) {
output[["mineral_N"]] <- sub.sipnet.output$minN * 0.001 # gN/m2 -> kgN/m2
output[["mineral_N"]] <- sub.sipnet.output$minN
}
if ("soilOrgN" %in% names(sub.sipnet.output)) {
output[["soil_organic_N"]] <- sub.sipnet.output$soilOrgN * 0.001 # kgN/m2
output[["soil_organic_N"]] <- sub.sipnet.output$soilOrgN
}
if ("litterN" %in% names(sub.sipnet.output)) {
output[["litter_N"]] <- sub.sipnet.output$litterN * 0.001 # kgN/m2
output[["litter_N"]] <- sub.sipnet.output$litterN
}
if ("n2o" %in% names(sub.sipnet.output)) {
output[["N2O_flux"]] <- PEcAn.utils::ud_convert(sub.sipnet.output$n2o, "g/m2", "kg/m2") / timestep.s
# convert g N m-2 per timestep -> kg N m-2 s-1
output[["N2O_flux"]] <- sub.sipnet.output$n2o
}
if ("nLeaching" %in% names(sub.sipnet.output)) {
output[["N_leaching"]] <- (sub.sipnet.output$nLeaching * 0.001) / timestep.s # kgN/m2/s
output[["N_leaching"]] <- sub.sipnet.output$nLeaching
}
if ("nFixation" %in% names(sub.sipnet.output)) {
output[["N_fixation"]] <- (sub.sipnet.output$nFixation * 0.001) / timestep.s # kgN/m2/s
output[["N_fixation"]] <- sub.sipnet.output$nFixation
}
if ("nUptake" %in% names(sub.sipnet.output)) {
output[["N_uptake"]] <- (sub.sipnet.output$nUptake * 0.001) / timestep.s # kgN/m2/s
output[["N_uptake"]] <- sub.sipnet.output$nUptake
}
if ("ch4" %in% names(sub.sipnet.output)) {
output[["CH4_flux"]] <- PEcAn.utils::ud_convert(sub.sipnet.output$ch4, "g/m2", "kg/m2") / timestep.s
# convert g C m-2 per timestep -> kg C m-2 s-1
output[["CH4_flux"]] <- sub.sipnet.output$ch4
}

output[["time_bounds"]] <- c(rbind(bounds[,1], bounds[,2]))

# ******************** Declare netCDF variables ********************#
Expand Down Expand Up @@ -376,10 +429,12 @@ model2netcdf.SIPNET <- function(outdir, sitelat, sitelon, start_date, end_date,
sipnet2datetime <- function(year, doy, hour){

hr <- floor(hour)
minsec <- PEcAn.utils::ud_convert(hour - hr, "hour", "min")
# minsec <- PEcAn.utils::ud_convert(hour - hr, "hour", "min")
minsec <- (hour - hr) * 60
minute <- floor(minsec)

sec <- PEcAn.utils::ud_convert(minsec - minute, "minute", "second")
# sec <- PEcAn.utils::ud_convert(minsec - minute, "minute", "second")
sec <- (minsec - minute) * 60

minute <- ifelse(sec == 60, minute + 1, minute)
sec <- ifelse(sec == 60, 0, sec)
Expand Down
5 changes: 3 additions & 2 deletions models/sipnet/man/model2netcdf.SIPNET.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 12 additions & 1 deletion models/sipnet/tests/testthat/test-model2netcdf.SIPNET.R
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ make_base_sipnet <- function(n = 4L) {
year = 2002,
day = rep(c(1, 2), each = n / 2, length.out = n),
time = rep(c(6, 18), length.out = n),
plantWoodC = 5000, plantLeafC = 200, woodCreation = 0.5,
plantWoodC = 5000, plantLeafC = 200,
soil = 10000, microbeC = 8, coarseRootC = 1200, fineRootC = 800,
litter = 400, soilWater = 14, soilWetnessFrac = 0.85, snow = 0,
npp = 0.05, nee = 0.10, cumNEE = cumsum(rep(0.1, n)),
Expand Down Expand Up @@ -226,6 +226,17 @@ test_that("fluxes are converted from gC/m2/timestep to kg/m2/sec", {

expect_equal(pec$GPP, sip$gpp / 1000 / ts)
expect_equal(pec$Transp, sip$fluxestranspiration * 10 / ts, tolerance = 1e-6)

sip2 <- make_v2_sipnet()
out2 <- out_dir <- setup_sipnet_test(sip2)$outdir
pec2 <- PEcAn.utils::read.output(
ncfiles = file.path(out2, "2002.nc"),
variables = c("GPP", "GWBI", "Transp"),
dataframe = TRUE,
verbose = FALSE,
print_summary = FALSE
)
expect_equal(pec2$GWBI, sip2$woodCreation / 1000 / ts, tolerance = 1e-6)
})


Expand Down
Loading