From 6e09b06cb193278558b120983087d41ccb64159b Mon Sep 17 00:00:00 2001 From: David LeBauer Date: Wed, 10 Sep 2025 23:38:15 -0400 Subject: [PATCH 01/30] Add write.events.SIPNET function with tests and documentation; update NAMESPACE --- models/sipnet/NAMESPACE | 1 + models/sipnet/R/write.events.SIPNET.R | 105 ++++++++++++++++++ models/sipnet/man/write.events.SIPNET.Rd | 48 ++++++++ models/sipnet/tests/testthat/test.met2model.R | 3 +- 4 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 models/sipnet/R/write.events.SIPNET.R create mode 100644 models/sipnet/man/write.events.SIPNET.Rd diff --git a/models/sipnet/NAMESPACE b/models/sipnet/NAMESPACE index e2cdb0e98d0..6969832c0aa 100644 --- a/models/sipnet/NAMESPACE +++ b/models/sipnet/NAMESPACE @@ -10,6 +10,7 @@ export(sipnet2datetime) export(split_inputs.SIPNET) export(veg2model.SIPNET) export(write.config.SIPNET) +export(write.events.SIPNET) export(write_restart.SIPNET) importFrom(dplyr,"%>%") importFrom(rlang,.data) diff --git a/models/sipnet/R/write.events.SIPNET.R b/models/sipnet/R/write.events.SIPNET.R new file mode 100644 index 00000000000..50ade1ced1f --- /dev/null +++ b/models/sipnet/R/write.events.SIPNET.R @@ -0,0 +1,105 @@ +## TODO: +## - integrate call into write.configs.SIPNET +## - parameterize planting allocation fractions +## - make sure files are written in correct output directory +## - map crops associated w/ planting and harvest --> PFTs; this will need to be handled separate from events.in +#' Write SIPNET events.in files from a PEcAn events.json +#' +#' Reads a single PEcAn events.json containing one or more site objects and +#' writes one SIPNET `events.in` file per site. Events are translated according to [SIPNET's `events.in` +#' specification](https://pecanproject.github.io/sipnet/parameters/#agronomic-events). +#' The writer expects inputs to already match the PEcAn MVP schema v0.1.0 naming and units where applicable. +#' +#' @details +#' - Supported `event_type` values: `tillage`, `planting`, `fertilization`, +#' `irrigation`, `harvest`. +#' - Units translated from PEcAn standard_vars to SIPNET events.in specification: +#' `kg/m^2` to `g/m^2`; irrigation `amount_mm` to `cm`. +#' - Planting allocation uses fixed internal parameters. Future work should use the same values +#' that are written to `sipnet.parms` (e.g. after integrating this into `write.configs.SIPNET`) +#' +#' @param events_json character. Path to an `events.json` file containing an +#' array of site objects with `site_id`, optional `pft`, and `events`. +#' @param outdir character. Output directory where per-site `events-.in` +#' files are written. +#' +#' @return Invisibly, a vector of files written. +#' +#' @examples +#' # Example with two events for a single site +#' tmp <- withr::local_tempfile(fileext = ".json") +#' site <- list( +#' site_id = "EX1", +#' events = list( +#' list(event_type = "tillage", date = "2022-02-04", tillage_eff_0to1 = 0.2), +#' list(event_type = "planting", date = "2022-02-19", leaf_c_kg_m2 = 0.01) +#' ) +#' ) +#' jsonlite::write_json(list(site), tmp, auto_unbox = TRUE) +#' outdir <- withr::local_tempdir() +#' files <- write.events.SIPNET(tmp, outdir) +#' files +#' +#' @export +write.events.SIPNET <- function(events_json, outdir) { + x <- jsonlite::fromJSON(events_json, simplifyVector = FALSE) + site_objs <- if (!is.null(x$site_id)) list(x) else x + files_written <- vector() + + leafAllocation <- 0.50 + woodAllocation <- 0.15 + fineRootAllocation <- 0.10 + coarseRootAllocation <- 0.25 + + # Unit conversion helpers + kg2g <- as.numeric(PEcAn.utils::ud_convert(1, "kg", "g")) # 1000 + mm2cm <- as.numeric(PEcAn.utils::ud_convert(1, "mm", "cm")) # 0.1 + + # For each site, build event time series and write file + for (site in site_objs) { + sid <- site$site_id + evs <- site$events + # Order by date and build lines + dates <- as.Date(vapply(evs, function(e) e$date, character(1))) + ord <- order(dates) + lines <- vector() + for (idx in ord) { + e <- evs[[idx]] + d <- as.Date(e$date) + year <- as.integer(format(d, "%Y")) + day <- as.integer(strftime(d, "%j")) + type <- e$event_type + if (type == "tillage") { + f <- if (is.null(e$tillage_eff_0to1)) 0 else e$tillage_eff_0to1 + lines <- c(lines, sprintf("%d %d till %s", year, day, f)) + } else if (type == "planting") { + # infer total planted biomass from leaf pool and allocation fraction + leaf_g <- as.numeric(if (is.null(e$leaf_c_kg_m2)) 0 else e$leaf_c_kg_m2) * kg2g + total_g <- if (leafAllocation > 0) leaf_g / leafAllocation else leaf_g + wood_g <- woodAllocation * total_g + fr_g <- fineRootAllocation * total_g + cr_g <- coarseRootAllocation * total_g + lines <- c(lines, sprintf("%d %d plant %s %s %s %s", year, day, leaf_g, wood_g, fr_g, cr_g)) + } else if (type == "fertilization") { + orgN_g <- as.numeric(if (is.null(e$org_n_kg_m2)) 0 else e$org_n_kg_m2) * kg2g + orgC_g <- as.numeric(if (is.null(e$org_c_kg_m2)) 0 else e$org_c_kg_m2) * kg2g + nh4_g <- as.numeric(if (is.null(e$nh4_n_kg_m2)) 0 else e$nh4_n_kg_m2) * kg2g + no3_g <- as.numeric(if (is.null(e$no3_n_kg_m2)) 0 else e$no3_n_kg_m2) * kg2g + minN_g <- nh4_g + no3_g + lines <- c(lines, sprintf("%d %d fert %s %s %s", year, day, orgN_g, orgC_g, minN_g)) + } else if (type == "irrigation") { + amt_cm <- as.numeric(if (is.null(e$amount_mm)) 0 else e$amount_mm) * mm2cm + method_code <- if (is.null(e$method) || e$method == "soil") 1 else 0 + lines <- c(lines, sprintf("%d %d irrig %s %s", year, day, amt_cm, method_code)) + } else if (type == "harvest") { + frac <- if (is.null(e$frac_above_removed_0to1)) 0 else e$frac_above_removed_0to1 + lines <- c(lines, sprintf("%d %d harv %s", year, day, frac)) + } + } + dir.create(outdir, showWarnings = FALSE, recursive = TRUE) + fp <- file.path(outdir, sprintf("events-%s.in", sid)) + writeLines(lines, fp) + files_written <- c(files_written, fp) + } + invisible(files_written) +} diff --git a/models/sipnet/man/write.events.SIPNET.Rd b/models/sipnet/man/write.events.SIPNET.Rd new file mode 100644 index 00000000000..153683a2c61 --- /dev/null +++ b/models/sipnet/man/write.events.SIPNET.Rd @@ -0,0 +1,48 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/write.events.SIPNET.R +\name{write.events.SIPNET} +\alias{write.events.SIPNET} +\title{Write SIPNET events.in files from a PEcAn events.json} +\usage{ +write.events.SIPNET(events_json, outdir) +} +\arguments{ +\item{events_json}{character. Path to an `events.json` file containing an +array of site objects with `site_id`, optional `pft`, and `events`.} + +\item{outdir}{character. Output directory where per-site `events-.in` +files are written.} +} +\value{ +Invisibly, a vector of files written. +} +\description{ +Reads a single PEcAn events.json containing one or more site objects and +writes one SIPNET `events.in` file per site. Events are translated according to [SIPNET's `events.in` +specification](https://pecanproject.github.io/sipnet/parameters/#agronomic-events). +The writer expects inputs to already match the PEcAn MVP schema v0.1.0 naming and units where applicable. +} +\details{ +- Supported `event_type` values: `tillage`, `planting`, `fertilization`, + `irrigation`, `harvest`. +- Units translated from PEcAn standard_vars to SIPNET events.in specification: + `kg/m^2` to `g/m^2`; irrigation `amount_mm` to `cm`. +- Planting allocation uses fixed internal parameters. Future work should use the same values + that are written to `sipnet.parms` (e.g. after integrating this into `write.configs.SIPNET`) +} +\examples{ +# Example with two events for a single site +tmp <- withr::local_tempfile(fileext = ".json") +site <- list( + site_id = "EX1", + events = list( + list(event_type = "tillage", date = "2022-02-04", tillage_eff_0to1 = 0.2), + list(event_type = "planting", date = "2022-02-19", leaf_c_kg_m2 = 0.01) + ) +) +jsonlite::write_json(list(site), tmp, auto_unbox = TRUE) +outdir <- withr::local_tempdir() +files <- write.events.SIPNET(tmp, outdir) +files + +} diff --git a/models/sipnet/tests/testthat/test.met2model.R b/models/sipnet/tests/testthat/test.met2model.R index b2dd3af4c7c..709f7d3a75a 100644 --- a/models/sipnet/tests/testthat/test.met2model.R +++ b/models/sipnet/tests/testthat/test.met2model.R @@ -17,7 +17,8 @@ add_gaps_to_nc <- function(src_nc, gapped_nc, test_that("Met conversion runs without error", { nc_path <- system.file("test-data", "CRUNCEP.2000.nc", - package = "PEcAn.utils") + package = "PEcAn.utils" + ) in.path <- dirname(nc_path) in.prefix <- "CRUNCEP" start_date <- "2000-01-01" From cbeae59e9ceee9ba8d4ad7b6589292ba3b21ecb2 Mon Sep 17 00:00:00 2001 From: David LeBauer Date: Wed, 10 Sep 2025 23:42:38 -0400 Subject: [PATCH 02/30] update changelog and news --- CHANGELOG.md | 4 +++- models/sipnet/NEWS.md | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ecbd01f6b5..abb2d0f7b71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,10 @@ # Change Log + All notable changes are kept in this file. All changes made should be added to the section called `Unreleased`. Once a new release is made this file will be updated to create a new `Unreleased` section for the next release. - For more information about this file see also [Keep a Changelog](http://keepachangelog.com/) . +For more information about this file see also [Keep a Changelog](http://keepachangelog.com/) . ## Unreleased @@ -13,6 +14,7 @@ section for the next release. * Add CH4 and N2O to standard_vars in PEcAn.utils * New function `sat_vapor_pressure()` added for computing saturation vapor pressure from temperature using various methods. * Added `AmeriFlux_met_ensemble()` function with ERA5 fallback for AmeriFlux meteorological data processing and ensemble generation +* `write.events.SIPNET()` to generate SIPNET `events.in` files from a `events.json` file. ## [1.9.0] - 2025-05-25 diff --git a/models/sipnet/NEWS.md b/models/sipnet/NEWS.md index e461f9f9fe3..b2ec82646d6 100644 --- a/models/sipnet/NEWS.md +++ b/models/sipnet/NEWS.md @@ -1,3 +1,9 @@ +# Unreleased + +## Added + +* `write.events.SIPNET()` to generate SIPNET `events.in` files from a `events.json` file. + # PEcAn.SIPNET 1.9.1 ## Changed From 0d0306da3a056c74deebc6bc03a568405c424c0e Mon Sep 17 00:00:00 2001 From: David LeBauer Date: Thu, 11 Sep 2025 00:43:08 -0400 Subject: [PATCH 03/30] create test events.json files instead of creating them on the fly --- .../testthat/event_fixtures/events_site1.json | 1 + .../event_fixtures/events_site1_site2.json | 35 ++++++++++++++++++ .../tests/testthat/test-write.events.SIPNET.R | 36 +++++++++++++++++++ 3 files changed, 72 insertions(+) create mode 100644 models/sipnet/tests/testthat/event_fixtures/events_site1.json create mode 100644 models/sipnet/tests/testthat/event_fixtures/events_site1_site2.json create mode 100644 models/sipnet/tests/testthat/test-write.events.SIPNET.R diff --git a/models/sipnet/tests/testthat/event_fixtures/events_site1.json b/models/sipnet/tests/testthat/event_fixtures/events_site1.json new file mode 100644 index 00000000000..207c7ba9177 --- /dev/null +++ b/models/sipnet/tests/testthat/event_fixtures/events_site1.json @@ -0,0 +1 @@ +[{"site_id":"EX1","events":[{"event_type":"tillage","date":"2022-02-04","tillage_eff_0to1":0.2},{"event_type":"tillage","date":"2022-02-09","tillage_eff_0to1":0.1},{"event_type":"irrigation","date":"2022-02-09","amount_mm":50,"method":"soil"},{"event_type":"fertilization","date":"2022-02-09","org_n_kg_m2":0,"org_c_kg_m2":0,"nh4_n_kg_m2":0.01},{"event_type":"planting","date":"2022-02-19","leaf_c_kg_m2":0.01},{"event_type":"harvest","date":"2022-09-07","frac_above_removed_0to1":0.1}]}] diff --git a/models/sipnet/tests/testthat/event_fixtures/events_site1_site2.json b/models/sipnet/tests/testthat/event_fixtures/events_site1_site2.json new file mode 100644 index 00000000000..c4b168a3d13 --- /dev/null +++ b/models/sipnet/tests/testthat/event_fixtures/events_site1_site2.json @@ -0,0 +1,35 @@ +[ + { + "site_id": "S1", + "pft": "PFT", + "events": [ + { + "event_type": "tillage", + "date": "2022-01-15", + "tillage_eff_0to1": 0.1 + }, + { + "event_type": "harvest", + "date": "2022-09-01", + "frac_above_removed_0to1": 0.2 + } + ] + }, + { + "site_id": "S2", + "pft": "PFT", + "events": [ + { + "event_type": "planting", + "date": "2022-03-01", + "leaf_c_kg_m2": 0.01 + }, + { + "event_type": "irrigation", + "date": "2022-03-10", + "amount_mm": 10, + "method": "soil" + } + ] + } +] diff --git a/models/sipnet/tests/testthat/test-write.events.SIPNET.R b/models/sipnet/tests/testthat/test-write.events.SIPNET.R new file mode 100644 index 00000000000..0be15f4392f --- /dev/null +++ b/models/sipnet/tests/testthat/test-write.events.SIPNET.R @@ -0,0 +1,36 @@ +context("write.events.SIPNET") + +# Helper to remove excess whitespace +norm <- function(x) gsub("\\s+", " ", trimws(x)) + +testthat::test_that("write.events.SIPNET produces expected lines", { + ev_json1 <- testthat::test_path("event_fixtures/events_site1.json") + outdir <- withr::local_tempdir() + files <- write.events.SIPNET(ev_json1, outdir) + expect_length(files, 1) + got <- readLines(files[1]) + expected <- c( + "2022 35 till 0.2", + "2022 40 till 0.1", + "2022 40 irrig 5 1", + "2022 40 fert 0 0 10", + "2022 50 plant 10 3 2 5", + "2022 250 harv 0.1" + ) + expect_equal(norm(got), norm(expected)) +}) + +testthat::test_that("write.events.SIPNET handles multi-site events.json (one file per site)", { + ev_json2 <- testthat::test_path("event_fixtures/events_site1_site2.json") + outdir <- withr::local_tempdir() + files <- write.events.SIPNET(ev_json2, outdir) + testthat::expect_length(files, 2) + testthat::expect_true(all(file.exists(files))) + # quick sanity checks for each site's first/last event ordering + got1 <- readLines(files[grepl("events-S1\\.in$", files)]) + got2 <- readLines(files[grepl("events-S2\\.in$", files)]) + testthat::expect_true(startsWith(norm(got1[1]), "2022 15 till")) + testthat::expect_true(startsWith(norm(tail(got1, 1)), "2022 244 harv")) + testthat::expect_true(startsWith(norm(got2[1]), "2022 60 plant")) + testthat::expect_true(startsWith(norm(tail(got2, 1)), "2022 69 irrig")) +}) From 6e277b4d0d0e8c3b3f992e1a4dee8016daf0ee17 Mon Sep 17 00:00:00 2001 From: David LeBauer Date: Thu, 11 Sep 2025 00:46:16 -0400 Subject: [PATCH 04/30] make json pretty --- .../testthat/event_fixtures/events_site1.json | 41 ++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/models/sipnet/tests/testthat/event_fixtures/events_site1.json b/models/sipnet/tests/testthat/event_fixtures/events_site1.json index 207c7ba9177..a5cd4ab19ca 100644 --- a/models/sipnet/tests/testthat/event_fixtures/events_site1.json +++ b/models/sipnet/tests/testthat/event_fixtures/events_site1.json @@ -1 +1,40 @@ -[{"site_id":"EX1","events":[{"event_type":"tillage","date":"2022-02-04","tillage_eff_0to1":0.2},{"event_type":"tillage","date":"2022-02-09","tillage_eff_0to1":0.1},{"event_type":"irrigation","date":"2022-02-09","amount_mm":50,"method":"soil"},{"event_type":"fertilization","date":"2022-02-09","org_n_kg_m2":0,"org_c_kg_m2":0,"nh4_n_kg_m2":0.01},{"event_type":"planting","date":"2022-02-19","leaf_c_kg_m2":0.01},{"event_type":"harvest","date":"2022-09-07","frac_above_removed_0to1":0.1}]}] +[ + { + "site_id": ["EX1"], + "events": [ + { + "event_type": ["tillage"], + "date": ["2022-02-04"], + "tillage_eff_0to1": [0.2] + }, + { + "event_type": ["tillage"], + "date": ["2022-02-09"], + "tillage_eff_0to1": [0.1] + }, + { + "event_type": ["irrigation"], + "date": ["2022-02-09"], + "amount_mm": [50], + "method": ["soil"] + }, + { + "event_type": ["fertilization"], + "date": ["2022-02-09"], + "org_n_kg_m2": [0], + "org_c_kg_m2": [0], + "nh4_n_kg_m2": [0.01] + }, + { + "event_type": ["planting"], + "date": ["2022-02-19"], + "leaf_c_kg_m2": [0.01] + }, + { + "event_type": ["harvest"], + "date": ["2022-09-07"], + "frac_above_removed_0to1": [0.1] + } + ] + } +] From fc3614119bda8dbd9083a8d149db7ca2d371253f Mon Sep 17 00:00:00 2001 From: David LeBauer Date: Wed, 24 Sep 2025 15:10:25 -0700 Subject: [PATCH 05/30] Update test.met2model.R Co-authored-by: Chris Black --- models/sipnet/tests/testthat/test.met2model.R | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/models/sipnet/tests/testthat/test.met2model.R b/models/sipnet/tests/testthat/test.met2model.R index 709f7d3a75a..9d1a0417e8c 100644 --- a/models/sipnet/tests/testthat/test.met2model.R +++ b/models/sipnet/tests/testthat/test.met2model.R @@ -16,7 +16,9 @@ add_gaps_to_nc <- function(src_nc, gapped_nc, } test_that("Met conversion runs without error", { - nc_path <- system.file("test-data", "CRUNCEP.2000.nc", + nc_path <- system.file( + "test-data", + "CRUNCEP.2000.nc", package = "PEcAn.utils" ) in.path <- dirname(nc_path) From bbe1dfc16e2f9cbd97a4521398609c5b7e2f3eb9 Mon Sep 17 00:00:00 2001 From: David LeBauer Date: Wed, 24 Sep 2025 21:41:59 -0700 Subject: [PATCH 06/30] Apply suggestion from @dlebauer --- models/sipnet/R/write.events.SIPNET.R | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/models/sipnet/R/write.events.SIPNET.R b/models/sipnet/R/write.events.SIPNET.R index 50ade1ced1f..9b72ca1b7e7 100644 --- a/models/sipnet/R/write.events.SIPNET.R +++ b/models/sipnet/R/write.events.SIPNET.R @@ -66,8 +66,7 @@ write.events.SIPNET <- function(events_json, outdir) { for (idx in ord) { e <- evs[[idx]] d <- as.Date(e$date) - year <- as.integer(format(d, "%Y")) - day <- as.integer(strftime(d, "%j")) + day <- as.integer(format(d, "%j")) type <- e$event_type if (type == "tillage") { f <- if (is.null(e$tillage_eff_0to1)) 0 else e$tillage_eff_0to1 From 40aad4a13afc2c1714b7433d4d8b9cfbc311a341 Mon Sep 17 00:00:00 2001 From: David LeBauer Date: Wed, 24 Sep 2025 21:42:59 -0700 Subject: [PATCH 07/30] Apply suggestion from @dlebauer --- models/sipnet/tests/testthat/test-write.events.SIPNET.R | 1 + 1 file changed, 1 insertion(+) diff --git a/models/sipnet/tests/testthat/test-write.events.SIPNET.R b/models/sipnet/tests/testthat/test-write.events.SIPNET.R index 0be15f4392f..2fc9934132e 100644 --- a/models/sipnet/tests/testthat/test-write.events.SIPNET.R +++ b/models/sipnet/tests/testthat/test-write.events.SIPNET.R @@ -18,6 +18,7 @@ testthat::test_that("write.events.SIPNET produces expected lines", { "2022 250 harv 0.1" ) expect_equal(norm(got), norm(expected)) + # TODO determine What's generating the whitespace differences and eliminate use of norm() }) testthat::test_that("write.events.SIPNET handles multi-site events.json (one file per site)", { From a42e506ab5b62ba6b71683cd9d29c3c75878f95f Mon Sep 17 00:00:00 2001 From: David LeBauer Date: Wed, 24 Sep 2025 21:44:01 -0700 Subject: [PATCH 08/30] Apply suggestion from @dlebauer --- models/sipnet/R/write.events.SIPNET.R | 1 + 1 file changed, 1 insertion(+) diff --git a/models/sipnet/R/write.events.SIPNET.R b/models/sipnet/R/write.events.SIPNET.R index 9b72ca1b7e7..92747bb78cf 100644 --- a/models/sipnet/R/write.events.SIPNET.R +++ b/models/sipnet/R/write.events.SIPNET.R @@ -70,6 +70,7 @@ write.events.SIPNET <- function(events_json, outdir) { type <- e$event_type if (type == "tillage") { f <- if (is.null(e$tillage_eff_0to1)) 0 else e$tillage_eff_0to1 + # TODO: consider validating up front against schema rather than here lines <- c(lines, sprintf("%d %d till %s", year, day, f)) } else if (type == "planting") { # infer total planted biomass from leaf pool and allocation fraction From b7bef4c805e2917c75a1a7d81a6a03fe428e3589 Mon Sep 17 00:00:00 2001 From: David LeBauer Date: Wed, 24 Sep 2025 21:45:06 -0700 Subject: [PATCH 09/30] Apply suggestion from @dlebauer --- models/sipnet/R/write.events.SIPNET.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/sipnet/R/write.events.SIPNET.R b/models/sipnet/R/write.events.SIPNET.R index 92747bb78cf..a58327802a9 100644 --- a/models/sipnet/R/write.events.SIPNET.R +++ b/models/sipnet/R/write.events.SIPNET.R @@ -62,7 +62,7 @@ write.events.SIPNET <- function(events_json, outdir) { # Order by date and build lines dates <- as.Date(vapply(evs, function(e) e$date, character(1))) ord <- order(dates) - lines <- vector() + lines <- vector(length = length(evs)) for (idx in ord) { e <- evs[[idx]] d <- as.Date(e$date) From 90c62e0018e79a70f65d5584481fe3fca298e2f4 Mon Sep 17 00:00:00 2001 From: David LeBauer Date: Wed, 24 Sep 2025 21:46:08 -0700 Subject: [PATCH 10/30] Apply suggestion from @dlebauer --- models/sipnet/R/write.events.SIPNET.R | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/models/sipnet/R/write.events.SIPNET.R b/models/sipnet/R/write.events.SIPNET.R index a58327802a9..3d62de4f705 100644 --- a/models/sipnet/R/write.events.SIPNET.R +++ b/models/sipnet/R/write.events.SIPNET.R @@ -63,8 +63,7 @@ write.events.SIPNET <- function(events_json, outdir) { dates <- as.Date(vapply(evs, function(e) e$date, character(1))) ord <- order(dates) lines <- vector(length = length(evs)) - for (idx in ord) { - e <- evs[[idx]] + for (e in evs) { d <- as.Date(e$date) day <- as.integer(format(d, "%j")) type <- e$event_type From 407a8c036240765b5470df27433f5746810e90c2 Mon Sep 17 00:00:00 2001 From: David LeBauer Date: Wed, 24 Sep 2025 21:50:13 -0700 Subject: [PATCH 11/30] Apply suggestion from @dlebauer --- models/sipnet/R/write.events.SIPNET.R | 1 + 1 file changed, 1 insertion(+) diff --git a/models/sipnet/R/write.events.SIPNET.R b/models/sipnet/R/write.events.SIPNET.R index 3d62de4f705..f3c25ebd016 100644 --- a/models/sipnet/R/write.events.SIPNET.R +++ b/models/sipnet/R/write.events.SIPNET.R @@ -42,6 +42,7 @@ #' #' @export write.events.SIPNET <- function(events_json, outdir) { + # TODO add overwrite argument x <- jsonlite::fromJSON(events_json, simplifyVector = FALSE) site_objs <- if (!is.null(x$site_id)) list(x) else x files_written <- vector() From 8fd9f810c71bd4cf62fa5d845d7211dd14f55262 Mon Sep 17 00:00:00 2001 From: David LeBauer Date: Wed, 24 Sep 2025 21:52:04 -0700 Subject: [PATCH 12/30] Apply suggestion from @dlebauer --- models/sipnet/R/write.events.SIPNET.R | 1 + 1 file changed, 1 insertion(+) diff --git a/models/sipnet/R/write.events.SIPNET.R b/models/sipnet/R/write.events.SIPNET.R index f3c25ebd016..a5bdbd49539 100644 --- a/models/sipnet/R/write.events.SIPNET.R +++ b/models/sipnet/R/write.events.SIPNET.R @@ -44,6 +44,7 @@ write.events.SIPNET <- function(events_json, outdir) { # TODO add overwrite argument x <- jsonlite::fromJSON(events_json, simplifyVector = FALSE) + # allow a single site events.json that does not have a site_id site_objs <- if (!is.null(x$site_id)) list(x) else x files_written <- vector() From 1508bfd347fdb10eeb5ed8bc526a6a099e021fd1 Mon Sep 17 00:00:00 2001 From: David LeBauer Date: Thu, 25 Sep 2025 16:35:39 -0700 Subject: [PATCH 13/30] Update write.events.SIPNET.R Co-authored-by: Chris Black --- models/sipnet/R/write.events.SIPNET.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/sipnet/R/write.events.SIPNET.R b/models/sipnet/R/write.events.SIPNET.R index a5bdbd49539..f593035de00 100644 --- a/models/sipnet/R/write.events.SIPNET.R +++ b/models/sipnet/R/write.events.SIPNET.R @@ -64,7 +64,7 @@ write.events.SIPNET <- function(events_json, outdir) { # Order by date and build lines dates <- as.Date(vapply(evs, function(e) e$date, character(1))) ord <- order(dates) - lines <- vector(length = length(evs)) + lines <- character() for (e in evs) { d <- as.Date(e$date) day <- as.integer(format(d, "%j")) From 5523a13abbbb97d27736a04a0267e37f00c08a52 Mon Sep 17 00:00:00 2001 From: David LeBauer Date: Thu, 25 Sep 2025 16:35:48 -0700 Subject: [PATCH 14/30] Update write.events.SIPNET.R Co-authored-by: Chris Black --- models/sipnet/R/write.events.SIPNET.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/sipnet/R/write.events.SIPNET.R b/models/sipnet/R/write.events.SIPNET.R index f593035de00..3bb68228e86 100644 --- a/models/sipnet/R/write.events.SIPNET.R +++ b/models/sipnet/R/write.events.SIPNET.R @@ -65,7 +65,7 @@ write.events.SIPNET <- function(events_json, outdir) { dates <- as.Date(vapply(evs, function(e) e$date, character(1))) ord <- order(dates) lines <- character() - for (e in evs) { + for (e in evs[ord]) { d <- as.Date(e$date) day <- as.integer(format(d, "%j")) type <- e$event_type From 780b877c1b97e09a8111583a41cb0c8225fc3b6c Mon Sep 17 00:00:00 2001 From: David LeBauer Date: Thu, 25 Sep 2025 16:36:02 -0700 Subject: [PATCH 15/30] Update write.events.SIPNET.R Co-authored-by: Chris Black --- models/sipnet/R/write.events.SIPNET.R | 1 + 1 file changed, 1 insertion(+) diff --git a/models/sipnet/R/write.events.SIPNET.R b/models/sipnet/R/write.events.SIPNET.R index 3bb68228e86..f9385457c96 100644 --- a/models/sipnet/R/write.events.SIPNET.R +++ b/models/sipnet/R/write.events.SIPNET.R @@ -67,6 +67,7 @@ write.events.SIPNET <- function(events_json, outdir) { lines <- character() for (e in evs[ord]) { d <- as.Date(e$date) + year <- as.integer(format(d, "%Y")) day <- as.integer(format(d, "%j")) type <- e$event_type if (type == "tillage") { From 82a86b5fa43b977648c538d8ec372fdcd2f17ce4 Mon Sep 17 00:00:00 2001 From: Chris Black Date: Tue, 30 Sep 2025 17:02:20 -0700 Subject: [PATCH 16/30] write all params of harvest event --- models/sipnet/R/write.events.SIPNET.R | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/models/sipnet/R/write.events.SIPNET.R b/models/sipnet/R/write.events.SIPNET.R index f9385457c96..2be553dd938 100644 --- a/models/sipnet/R/write.events.SIPNET.R +++ b/models/sipnet/R/write.events.SIPNET.R @@ -94,8 +94,19 @@ write.events.SIPNET <- function(events_json, outdir) { method_code <- if (is.null(e$method) || e$method == "soil") 1 else 0 lines <- c(lines, sprintf("%d %d irrig %s %s", year, day, amt_cm, method_code)) } else if (type == "harvest") { - frac <- if (is.null(e$frac_above_removed_0to1)) 0 else e$frac_above_removed_0to1 - lines <- c(lines, sprintf("%d %d harv %s", year, day, frac)) + abv_rem <- e$frac_above_removed_0to1 %||% 0 + blw_rem <- e$frac_below_removed_0to1 %||% 0 + abv_lit <- e$frac_above_to_litter_0to1 %||% (1.0 - abv_rem) + blw_lit <- e$frac_below_to_litter_0to1 %||% (1.0 - blw_rem) + lines <- c( + lines, + sprintf( + "%d %d harv %s %s %s %s", + year, day, + abv_rem, blw_rem, + abv_lit, blw_lit + ) + ) } } dir.create(outdir, showWarnings = FALSE, recursive = TRUE) From 595a7b64099bfdcba389a71277b0c1b46a0b7958 Mon Sep 17 00:00:00 2001 From: Chris Black Date: Tue, 30 Sep 2025 17:05:34 -0700 Subject: [PATCH 17/30] declare jsonlite import --- docker/depends/pecan_package_dependencies.csv | 1 + models/sipnet/DESCRIPTION | 1 + 2 files changed, 2 insertions(+) diff --git a/docker/depends/pecan_package_dependencies.csv b/docker/depends/pecan_package_dependencies.csv index 96dc8b7399e..f4d17aea767 100644 --- a/docker/depends/pecan_package_dependencies.csv +++ b/docker/depends/pecan_package_dependencies.csv @@ -131,6 +131,7 @@ "IDPmisc","*","modules/assim.batch","Imports",FALSE "itertools","*","modules/assim.sequential","Suggests",FALSE "jsonlite","*","base/remote","Imports",FALSE +"jsonlite","*","models/sipnet","Imports",FALSE "jsonlite","*","models/stics","Imports",FALSE "jsonlite","*","modules/data.atmosphere","Imports",FALSE "jsonlite","*","modules/data.remote","Suggests",FALSE diff --git a/models/sipnet/DESCRIPTION b/models/sipnet/DESCRIPTION index 6017d9309e5..16dd9ff3447 100644 --- a/models/sipnet/DESCRIPTION +++ b/models/sipnet/DESCRIPTION @@ -14,6 +14,7 @@ URL: https://pecanproject.github.io BugReports: https://github.com/PecanProject/pecan/issues Imports: dplyr, + jsonlite, lubridate (>= 1.6.0), ncdf4 (>= 1.15), PEcAn.data.atmosphere, From 9d534f6f5b11d8fea516679f272dee9c0493b289 Mon Sep 17 00:00:00 2001 From: Chris Black Date: Tue, 30 Sep 2025 20:45:56 -0700 Subject: [PATCH 18/30] add harv params in tests --- .../sipnet/tests/testthat/event_fixtures/events_site1.json | 5 ++++- .../tests/testthat/event_fixtures/events_site1_site2.json | 5 ++++- models/sipnet/tests/testthat/test-write.events.SIPNET.R | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/models/sipnet/tests/testthat/event_fixtures/events_site1.json b/models/sipnet/tests/testthat/event_fixtures/events_site1.json index a5cd4ab19ca..a57e9f24a1c 100644 --- a/models/sipnet/tests/testthat/event_fixtures/events_site1.json +++ b/models/sipnet/tests/testthat/event_fixtures/events_site1.json @@ -33,7 +33,10 @@ { "event_type": ["harvest"], "date": ["2022-09-07"], - "frac_above_removed_0to1": [0.1] + "frac_above_removed_0to1": [0.1], + "frac_below_removed_0to1": [0.0], + "frac_above_to_litter_0to1": [0.0], + "frac_below_to_litter_0to1": [0.0] } ] } diff --git a/models/sipnet/tests/testthat/event_fixtures/events_site1_site2.json b/models/sipnet/tests/testthat/event_fixtures/events_site1_site2.json index c4b168a3d13..16f8c0be76d 100644 --- a/models/sipnet/tests/testthat/event_fixtures/events_site1_site2.json +++ b/models/sipnet/tests/testthat/event_fixtures/events_site1_site2.json @@ -11,7 +11,10 @@ { "event_type": "harvest", "date": "2022-09-01", - "frac_above_removed_0to1": 0.2 + "frac_above_removed_0to1": 0.2, + "frac_below_removed_0to1": 0.0, + "frac_above_to_litter_0to1": 0.0, + "frac_below_to_litter_0to1": 0.0 } ] }, diff --git a/models/sipnet/tests/testthat/test-write.events.SIPNET.R b/models/sipnet/tests/testthat/test-write.events.SIPNET.R index 2fc9934132e..881062902e2 100644 --- a/models/sipnet/tests/testthat/test-write.events.SIPNET.R +++ b/models/sipnet/tests/testthat/test-write.events.SIPNET.R @@ -15,7 +15,7 @@ testthat::test_that("write.events.SIPNET produces expected lines", { "2022 40 irrig 5 1", "2022 40 fert 0 0 10", "2022 50 plant 10 3 2 5", - "2022 250 harv 0.1" + "2022 250 harv 0.1 0 0 0" ) expect_equal(norm(got), norm(expected)) # TODO determine What's generating the whitespace differences and eliminate use of norm() From 7054620ecbd53fc84d47d0ded2aa12976271104e Mon Sep 17 00:00:00 2001 From: Chris Black Date: Fri, 3 Oct 2025 15:40:41 -0700 Subject: [PATCH 19/30] import %||% --- models/sipnet/NAMESPACE | 1 + models/sipnet/R/write.events.SIPNET.R | 1 + 2 files changed, 2 insertions(+) diff --git a/models/sipnet/NAMESPACE b/models/sipnet/NAMESPACE index 4b7560ceea0..efa0bc19570 100644 --- a/models/sipnet/NAMESPACE +++ b/models/sipnet/NAMESPACE @@ -12,4 +12,5 @@ export(write.config.SIPNET) export(write.events.SIPNET) export(write_restart.SIPNET) importFrom(dplyr,"%>%") +importFrom(rlang,"%||%") importFrom(rlang,.data) diff --git a/models/sipnet/R/write.events.SIPNET.R b/models/sipnet/R/write.events.SIPNET.R index 2be553dd938..f8dd47ec145 100644 --- a/models/sipnet/R/write.events.SIPNET.R +++ b/models/sipnet/R/write.events.SIPNET.R @@ -40,6 +40,7 @@ #' files <- write.events.SIPNET(tmp, outdir) #' files #' +#' @importFrom rlang %||% #' @export write.events.SIPNET <- function(events_json, outdir) { # TODO add overwrite argument From f800475a3caa88ed75e5b4532590500ae175e777 Mon Sep 17 00:00:00 2001 From: David LeBauer Date: Wed, 15 Oct 2025 09:23:11 -0700 Subject: [PATCH 20/30] fix build error --- models/sipnet/R/write.events.SIPNET.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/sipnet/R/write.events.SIPNET.R b/models/sipnet/R/write.events.SIPNET.R index f8dd47ec145..f2cef26eb61 100644 --- a/models/sipnet/R/write.events.SIPNET.R +++ b/models/sipnet/R/write.events.SIPNET.R @@ -63,7 +63,7 @@ write.events.SIPNET <- function(events_json, outdir) { sid <- site$site_id evs <- site$events # Order by date and build lines - dates <- as.Date(vapply(evs, function(e) e$date, character(1))) + dates <- as.Date(vapply(evs, function(e) ac.character(e$date), character(1))) ord <- order(dates) lines <- character() for (e in evs[ord]) { From bf3b456a87dca799e4d948686415433ac0c81861 Mon Sep 17 00:00:00 2001 From: David LeBauer Date: Wed, 15 Oct 2025 11:00:44 -0700 Subject: [PATCH 21/30] typo --- models/sipnet/R/write.events.SIPNET.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/sipnet/R/write.events.SIPNET.R b/models/sipnet/R/write.events.SIPNET.R index f2cef26eb61..7af16e00f0f 100644 --- a/models/sipnet/R/write.events.SIPNET.R +++ b/models/sipnet/R/write.events.SIPNET.R @@ -63,7 +63,7 @@ write.events.SIPNET <- function(events_json, outdir) { sid <- site$site_id evs <- site$events # Order by date and build lines - dates <- as.Date(vapply(evs, function(e) ac.character(e$date), character(1))) + dates <- as.Date(vapply(evs, function(e) as.character(e$date), character(1))) ord <- order(dates) lines <- character() for (e in evs[ord]) { From b57bd58aa5bf68de0a105032fc46a88f1824c099 Mon Sep 17 00:00:00 2001 From: David LeBauer Date: Wed, 15 Oct 2025 14:53:18 -0700 Subject: [PATCH 22/30] New function to validate events.json files; moved example events.json from sipnet to data.land package --- docker/depends/pecan_package_dependencies.csv | 2 + models/sipnet/DESCRIPTION | 1 + models/sipnet/R/write.events.SIPNET.R | 5 +- .../testthat/event_fixtures/events_site1.json | 43 ------ .../tests/testthat/test-write.events.SIPNET.R | 12 +- modules/data.land/DESCRIPTION | 1 + modules/data.land/NAMESPACE | 1 + .../R/look_up_fertilizer_components.R | 48 +++---- modules/data.land/R/validate_events.R | 53 +++++++ .../inst/events_fixtures/events_site1.json | 44 ++++++ .../events_fixtures}/events_site1_site2.json | 2 + .../data.land/inst/events_schema_v0.1.0.json | 135 ++++++++++-------- modules/data.land/man/validate_events_json.Rd | 35 +++++ .../testthat/test-validate_events_json.R | 15 ++ 14 files changed, 263 insertions(+), 134 deletions(-) delete mode 100644 models/sipnet/tests/testthat/event_fixtures/events_site1.json create mode 100644 modules/data.land/R/validate_events.R create mode 100644 modules/data.land/inst/events_fixtures/events_site1.json rename {models/sipnet/tests/testthat/event_fixtures => modules/data.land/inst/events_fixtures}/events_site1_site2.json (90%) create mode 100644 modules/data.land/man/validate_events_json.Rd create mode 100644 modules/data.land/tests/testthat/test-validate_events_json.R diff --git a/docker/depends/pecan_package_dependencies.csv b/docker/depends/pecan_package_dependencies.csv index f4d17aea767..4b626db69a4 100644 --- a/docker/depends/pecan_package_dependencies.csv +++ b/docker/depends/pecan_package_dependencies.csv @@ -135,6 +135,8 @@ "jsonlite","*","models/stics","Imports",FALSE "jsonlite","*","modules/data.atmosphere","Imports",FALSE "jsonlite","*","modules/data.remote","Suggests",FALSE +"jsonvalidate","*","models/sipnet","Suggests",FALSE +"jsonvalidate","*","modules/data.land","Suggests",FALSE "keras3",">= 1.0.0","modules/assim.sequential","Suggests",FALSE "knitr","*","base/visualization","Suggests",FALSE "knitr","*","models/biocro","Suggests",FALSE diff --git a/models/sipnet/DESCRIPTION b/models/sipnet/DESCRIPTION index 16dd9ff3447..2ad29266581 100644 --- a/models/sipnet/DESCRIPTION +++ b/models/sipnet/DESCRIPTION @@ -26,6 +26,7 @@ Imports: stats Suggests: coda, + jsonvalidate, testthat (>= 1.0.2), withr SystemRequirements: SIPNET ecosystem model diff --git a/models/sipnet/R/write.events.SIPNET.R b/models/sipnet/R/write.events.SIPNET.R index 7af16e00f0f..d95111561e1 100644 --- a/models/sipnet/R/write.events.SIPNET.R +++ b/models/sipnet/R/write.events.SIPNET.R @@ -43,9 +43,12 @@ #' @importFrom rlang %||% #' @export write.events.SIPNET <- function(events_json, outdir) { + # Validate input JSON against PEcAn events schema + PEcAn.data.land::validate_events_json(events_json) + # TODO add overwrite argument x <- jsonlite::fromJSON(events_json, simplifyVector = FALSE) - # allow a single site events.json that does not have a site_id + # allow a single site events.json that does not have a site_id site_objs <- if (!is.null(x$site_id)) list(x) else x files_written <- vector() diff --git a/models/sipnet/tests/testthat/event_fixtures/events_site1.json b/models/sipnet/tests/testthat/event_fixtures/events_site1.json deleted file mode 100644 index a57e9f24a1c..00000000000 --- a/models/sipnet/tests/testthat/event_fixtures/events_site1.json +++ /dev/null @@ -1,43 +0,0 @@ -[ - { - "site_id": ["EX1"], - "events": [ - { - "event_type": ["tillage"], - "date": ["2022-02-04"], - "tillage_eff_0to1": [0.2] - }, - { - "event_type": ["tillage"], - "date": ["2022-02-09"], - "tillage_eff_0to1": [0.1] - }, - { - "event_type": ["irrigation"], - "date": ["2022-02-09"], - "amount_mm": [50], - "method": ["soil"] - }, - { - "event_type": ["fertilization"], - "date": ["2022-02-09"], - "org_n_kg_m2": [0], - "org_c_kg_m2": [0], - "nh4_n_kg_m2": [0.01] - }, - { - "event_type": ["planting"], - "date": ["2022-02-19"], - "leaf_c_kg_m2": [0.01] - }, - { - "event_type": ["harvest"], - "date": ["2022-09-07"], - "frac_above_removed_0to1": [0.1], - "frac_below_removed_0to1": [0.0], - "frac_above_to_litter_0to1": [0.0], - "frac_below_to_litter_0to1": [0.0] - } - ] - } -] diff --git a/models/sipnet/tests/testthat/test-write.events.SIPNET.R b/models/sipnet/tests/testthat/test-write.events.SIPNET.R index 881062902e2..ec00ada9b8e 100644 --- a/models/sipnet/tests/testthat/test-write.events.SIPNET.R +++ b/models/sipnet/tests/testthat/test-write.events.SIPNET.R @@ -3,8 +3,16 @@ context("write.events.SIPNET") # Helper to remove excess whitespace norm <- function(x) gsub("\\s+", " ", trimws(x)) +# Helper to locate fixtures from PEcAn.data.land, with source fallback +fixture_path <- function(name) { + p <- system.file(file.path("events_fixtures", name), package = "PEcAn.data.land") + if (!is.null(p) && nzchar(p)) return(p) + # Fallback to source path in monorepo when PEcAn.data.land isn't installed + testthat::test_path(file.path("../../../../modules/data.land/inst/events_fixtures", name)) +} + testthat::test_that("write.events.SIPNET produces expected lines", { - ev_json1 <- testthat::test_path("event_fixtures/events_site1.json") + ev_json1 <- fixture_path("events_site1.json") outdir <- withr::local_tempdir() files <- write.events.SIPNET(ev_json1, outdir) expect_length(files, 1) @@ -22,7 +30,7 @@ testthat::test_that("write.events.SIPNET produces expected lines", { }) testthat::test_that("write.events.SIPNET handles multi-site events.json (one file per site)", { - ev_json2 <- testthat::test_path("event_fixtures/events_site1_site2.json") + ev_json2 <- fixture_path("events_site1_site2.json") outdir <- withr::local_tempdir() files <- write.events.SIPNET(ev_json2, outdir) testthat::expect_length(files, 2) diff --git a/modules/data.land/DESCRIPTION b/modules/data.land/DESCRIPTION index 6707afac80a..126a3a87f0d 100644 --- a/modules/data.land/DESCRIPTION +++ b/modules/data.land/DESCRIPTION @@ -63,6 +63,7 @@ Imports: Suggests: dataone, datapack, + jsonvalidate, getPass, glue, PEcAn.settings, diff --git a/modules/data.land/NAMESPACE b/modules/data.land/NAMESPACE index 634d8850e0c..84e1d16b14d 100644 --- a/modules/data.land/NAMESPACE +++ b/modules/data.land/NAMESPACE @@ -66,6 +66,7 @@ export(soilgrids_texture_extraction) export(subset_layer) export(to.Tag) export(to.TreeCode) +export(validate_events_json) export(write_ic) export(write_veg) importFrom(dplyr,"%>%") diff --git a/modules/data.land/R/look_up_fertilizer_components.R b/modules/data.land/R/look_up_fertilizer_components.R index ed6a7818d50..7e35d4ffd79 100644 --- a/modules/data.land/R/look_up_fertilizer_components.R +++ b/modules/data.land/R/look_up_fertilizer_components.R @@ -1,30 +1,30 @@ #' Calculate the Nitrogen and Carbon Content of a Fertilizer Application #' #' This function calculates the different forms of nitrogen (NO3-N, NH4-N, organic N) and organic carbon (C_org) in a fertilizer application. -#' It can determine fertilizer nitrogen and carbon content using either a lookup table based on +#' It can determine fertilizer nitrogen and carbon content using either a lookup table based on #' the SWAT model's [`fertilizer.frt`](https://github.com/swat-model/swatplus/blob/main/data/Osu_1hru/fertilizer.frt) -#' file, determine the fertilizer's nutrient content based on NN-PP-KK format, or use user-specified +#' file, determine the fertilizer's nutrient content based on NN-PP-KK format, or use user-specified #' fractions of organic nitrogen and carbon. #' -#' Consistent with assumptions in DayCent, DSSAT, and other models, urea is treated as NH3 because the +#' Consistent with assumptions in DayCent, DSSAT, and other models, urea is treated as NH3 because the #' transformation typically occurs within a day. -#' -#' @param type Character string specifying the type of fertilizer. Valid values include NN-PP-KK format (e.g., "45-5-10") as well +#' +#' @param type Character string specifying the type of fertilizer. Valid values include NN-PP-KK format (e.g., "45-5-10") as well #' as enumerated types including: "urea", "ammonium_nitrate", "compost", "manure", "dairy_fr", "beef_fr". #' See notes for full list of valid types. #' @param amount Numeric value specifying the amount of fertilizer applied in kg/ha. -#' @param fraction_organic_n Optional numeric value specifying the fraction of the organic matter that is nitrogen. +#' @param fraction_organic_n Optional numeric value specifying the fraction of the organic matter that is nitrogen. #' Used to define organic matter additions if not provided in the dataset. -#' @param fraction_organic_c Optional numeric value specifying the fraction of the organic matter that is carbon. +#' @param fraction_organic_c Optional numeric value specifying the fraction of the organic matter that is carbon. #' Used to define organic matter additions if not provided in the dataset. #' #' @md #' @note The following is a list of valid fertilizer names: #' - Mineral fertilizers: ammonium_nitrate, anhydrous_ammonia, urea -#' - Fresh manures: manure, beef_fr, broil_fr, dairy_fr, duck_fr, goat_fr, horse_fr, +#' - Fresh manures: manure, beef_fr, broil_fr, dairy_fr, duck_fr, goat_fr, horse_fr, #' layer_fr, sheep_fr, swine_fr, trkey_fr, veal_fr #' - Compost: org_compost -#' +#' #' @return A list containing: #' - `type`: The type of fertilizer used. #' - `NO3_N`: The amount of nitrate nitrogen (NO3-N) in kg/ha. @@ -35,7 +35,7 @@ #' @examples #' # View all available fertilizer types #' unique(PEcAn.data.land::fertilizer_composition_data$name) -#' +#' #' # Calculate components for different fertilizer types #' look_up_fertilizer_components("urea", 200) #' look_up_fertilizer_components("45-00-00", 200) @@ -45,12 +45,10 @@ #' #' @export look_up_fertilizer_components <- function( - type, - amount, - fraction_organic_n = NULL, - fraction_organic_c = NULL) { - - + type, + amount, + fraction_organic_n = NULL, + fraction_organic_c = NULL) { # Validate input for organic fertilizers if (!is.null(fraction_organic_n) || !is.null(fraction_organic_c)) { if (is.null(fraction_organic_n) || is.null(fraction_organic_c)) { @@ -58,7 +56,7 @@ look_up_fertilizer_components <- function( # could also make an assumption, but that seems error prone } } - + # If user provided organic matter fractions, use those regardless of whether they are in the database if (!is.null(fraction_organic_n) && !is.null(fraction_organic_c)) { return(list( @@ -69,7 +67,7 @@ look_up_fertilizer_components <- function( C_org = round(amount * fraction_organic_c) )) } - + # If not in the database, check if the fertilizer type is in NN-PP-KK format (e.g., 45-5-10) if (stringr::str_detect(type, "^\\d{1,2}-\\d{1,2}-\\d{1,2}$")) { # Split NN-PP-KK format into components @@ -90,9 +88,9 @@ look_up_fertilizer_components <- function( C_org = 0 )) } - + # Handle the case where the fertilizer type is in the database - if (type %in% PEcAn.data.land::fertilizer_composition_data$name) { + if (type %in% PEcAn.data.land::fertilizer_composition_data$name) { # Calculate the components directly in the data frame fertilizer_info <- PEcAn.data.land::fertilizer_composition_data |> dplyr::filter(.data$name == type) |> @@ -102,11 +100,11 @@ look_up_fertilizer_components <- function( N_org = round(amount * .data$fraction_organic_n), C_org = round(amount * .data$fraction_c) ) - - res <- fertilizer_info |> - dplyr::select("name", "NO3_N", "NH4_N", "N_org", "C_org") |> - dplyr::rename(type = .data$name) |> - as.list() + + res <- fertilizer_info |> + dplyr::select("name", "NO3_N", "NH4_N", "N_org", "C_org") |> + dplyr::rename(type = "name") |> + as.list() return(res) } else { PEcAn.logger::logger.error(paste("Fertilizer type", type, "not found in the database.")) diff --git a/modules/data.land/R/validate_events.R b/modules/data.land/R/validate_events.R new file mode 100644 index 00000000000..1b550f2a707 --- /dev/null +++ b/modules/data.land/R/validate_events.R @@ -0,0 +1,53 @@ +#' Validate PEcAn events JSON against schema v0.1.0 +#' +#' Validates a PEcAn events JSON file (single-site object or an array of site +#' objects) against the bundled JSON Schema (draft 2020-12) using the AJV +#' engine. +#' +#' - Logs an error and returns FALSE if the JSON file does not exist or does +#' not conform to the schema. +#' - Logs a warning and returns TRUE if the optional package `jsonvalidate` is +#' not installed, so calling code can proceed without a hard dependency. +#' +#' @param events_json character. Path to the JSON file to validate. +#' @param verbose logical. When `TRUE`, include detailed AJV messages on error. +#' +#' @return Logical TRUE if valid (or validator unavailable), FALSE if invalid. +#' +#' @author David LeBauer +#' +#' @examples +#' # validate_events_json(system.file("events_fixtures/events_site1.json", +#' # package = "PEcAn.data.land")) +#' +#' @export +validate_events_json <- function(events_json, verbose = TRUE) { + if (!file.exists(events_json)) { + PEcAn.logger::logger.error(glue::glue("events_json file does not exist: {events_json}")) + return(FALSE) + } + + if (!requireNamespace("jsonvalidate", quietly = TRUE)) { + PEcAn.logger::logger.warn("Skipping events schema validation: package 'jsonvalidate' not installed.") + return(TRUE) + } + + schema <- system.file("events_schema_v0.1.0.json", package = "PEcAn.data.land", mustWork = TRUE) + ok <- jsonvalidate::json_validate(events_json, schema = schema, engine = "ajv", verbose = verbose, error = FALSE) + if (isTRUE(ok)) { + PEcAn.logger::logger.info(glue::glue("events_json file is valid: {events_json}")) + return(TRUE) + } + + errs <- attr(ok, "errors") + detail <- if (is.null(errs)) { + "" + } else { + paste(sprintf( + "%s: %s", + ifelse(nzchar(errs$instancePath), errs$instancePath, ""), errs$message + ), collapse = "; ") + } + PEcAn.logger::logger.error(glue::glue("events_json does not conform to schema: {events_json}; {detail}")) + FALSE +} diff --git a/modules/data.land/inst/events_fixtures/events_site1.json b/modules/data.land/inst/events_fixtures/events_site1.json new file mode 100644 index 00000000000..19605953a19 --- /dev/null +++ b/modules/data.land/inst/events_fixtures/events_site1.json @@ -0,0 +1,44 @@ +[ + { + "pecan_events_version": "0.1.0", + "site_id": "EX1", + "events": [ + { + "event_type": "tillage", + "date": "2022-02-04", + "tillage_eff_0to1": 0.2 + }, + { + "event_type": "tillage", + "date": "2022-02-09", + "tillage_eff_0to1": 0.1 + }, + { + "event_type": "irrigation", + "date": "2022-02-09", + "amount_mm": 50, + "method": "soil" + }, + { + "event_type": "fertilization", + "date": "2022-02-09", + "org_n_kg_m2": 0, + "org_c_kg_m2": 0, + "nh4_n_kg_m2": 0.01 + }, + { + "event_type": "planting", + "date": "2022-02-19", + "leaf_c_kg_m2": 0.01 + }, + { + "event_type": "harvest", + "date": "2022-09-07", + "frac_above_removed_0to1": 0.1, + "frac_below_removed_0to1": 0.0, + "frac_above_to_litter_0to1": 0.0, + "frac_below_to_litter_0to1": 0.0 + } + ] + } +] diff --git a/models/sipnet/tests/testthat/event_fixtures/events_site1_site2.json b/modules/data.land/inst/events_fixtures/events_site1_site2.json similarity index 90% rename from models/sipnet/tests/testthat/event_fixtures/events_site1_site2.json rename to modules/data.land/inst/events_fixtures/events_site1_site2.json index 16f8c0be76d..84a2ee195d6 100644 --- a/models/sipnet/tests/testthat/event_fixtures/events_site1_site2.json +++ b/modules/data.land/inst/events_fixtures/events_site1_site2.json @@ -1,5 +1,6 @@ [ { + "pecan_events_version": "0.1.0", "site_id": "S1", "pft": "PFT", "events": [ @@ -19,6 +20,7 @@ ] }, { + "pecan_events_version": "0.1.0", "site_id": "S2", "pft": "PFT", "events": [ diff --git a/modules/data.land/inst/events_schema_v0.1.0.json b/modules/data.land/inst/events_schema_v0.1.0.json index 21a853f8bc3..caee982ef5a 100644 --- a/modules/data.land/inst/events_schema_v0.1.0.json +++ b/modules/data.land/inst/events_schema_v0.1.0.json @@ -1,73 +1,82 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://pecanproject.org/schema/events-mvp-0-1-0.json", - "type": "object", - "required": ["pecan_events_version", "site_id", "events"], - "properties": { - "pecan_events_version": { "type": "string", "const": "0.1.0" }, - "site_id": { "type": "string", "minLength": 1 }, - "ensemble_id": { "type": ["string", "null"], "minLength": 1 }, - "geometry_uri": { "type": ["string", "null"], "format": "uri" }, - "provenance": { "type": "object", "additionalProperties": true }, - "events": { - "type": "array", - "items": { - "type": "object", - "required": ["event_type", "date"], - "properties": { - "event_type": { - "type": "string", - "enum": ["planting", "harvest", "irrigation", "fertilization", "tillage"] - }, - "date": { "type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$" }, - "fraction_area": { "type": "number", "minimum": 0, "maximum": 1, "default": 1.0 }, - "source": { "type": "string" }, + "oneOf": [ + { "$ref": "#/$defs/site" }, + { "type": "array", "items": { "$ref": "#/$defs/site" } } + ], + "$defs": { + "site": { + "type": "object", + "required": ["pecan_events_version", "site_id", "events"], + "properties": { + "pecan_events_version": { "type": "string", "const": "0.1.0" }, + "site_id": { "type": "string", "minLength": 1 }, + "pft": { "type": "string" }, + "ensemble_id": { "type": ["string", "null"], "minLength": 1 }, + "geometry_uri": { "type": ["string", "null"], "format": "uri" }, + "provenance": { "type": "object", "additionalProperties": true }, + "events": { + "type": "array", + "items": { + "type": "object", + "required": ["event_type", "date"], + "properties": { + "event_type": { + "type": "string", + "enum": ["planting", "harvest", "irrigation", "fertilization", "tillage"] + }, + "date": { "type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$" }, + "fraction_area": { "type": "number", "minimum": 0, "maximum": 1, "default": 1.0 }, + "source": { "type": "string" }, - "leaf_c_kg_m2": { "type": "number", "minimum": 0 }, - "wood_c_kg_m2": { "type": "number", "minimum": 0 }, - "fine_root_c_kg_m2": { "type": "number", "minimum": 0 }, - "coarse_root_c_kg_m2": { "type": "number", "minimum": 0 }, - "cultivar": { "type": "string" }, - "crop_code": { "type": "string" }, - "crop_display": { "type": "string" }, + "leaf_c_kg_m2": { "type": "number", "minimum": 0 }, + "wood_c_kg_m2": { "type": "number", "minimum": 0 }, + "fine_root_c_kg_m2": { "type": "number", "minimum": 0 }, + "coarse_root_c_kg_m2": { "type": "number", "minimum": 0 }, + "cultivar": { "type": "string" }, + "crop_code": { "type": "string" }, + "crop_display": { "type": "string" }, - "frac_above_removed_0to1": { "type": "number", "minimum": 0, "maximum": 1 }, - "frac_below_removed_0to1": { "type": "number", "minimum": 0, "maximum": 1 }, - "frac_above_to_litter_0to1": { "type": "number", "minimum": 0, "maximum": 1 }, - "frac_below_to_litter_0to1": { "type": "number", "minimum": 0, "maximum": 1 }, + "frac_above_removed_0to1": { "type": "number", "minimum": 0, "maximum": 1 }, + "frac_below_removed_0to1": { "type": "number", "minimum": 0, "maximum": 1 }, + "frac_above_to_litter_0to1": { "type": "number", "minimum": 0, "maximum": 1 }, + "frac_below_to_litter_0to1": { "type": "number", "minimum": 0, "maximum": 1 }, - "amount_mm": { "type": "number", "minimum": 0 }, - "method": { "type": "string", "enum": ["soil", "canopy", "flood"] }, - "immed_evap_frac_0to1": { "type": "number", "minimum": 0, "maximum": 1 }, + "amount_mm": { "type": "number", "minimum": 0 }, + "method": { "type": "string", "enum": ["soil", "canopy", "flood"] }, + "immed_evap_frac_0to1": { "type": "number", "minimum": 0, "maximum": 1 }, - "org_c_kg_m2": { "type": "number", "minimum": 0 }, - "org_n_kg_m2": { "type": "number", "minimum": 0 }, - "nh4_n_kg_m2": { "type": "number", "minimum": 0 }, - "no3_n_kg_m2": { "type": "number", "minimum": 0 }, + "org_c_kg_m2": { "type": "number", "minimum": 0 }, + "org_n_kg_m2": { "type": "number", "minimum": 0 }, + "nh4_n_kg_m2": { "type": "number", "minimum": 0 }, + "no3_n_kg_m2": { "type": "number", "minimum": 0 }, - "tillage_eff_0to1": { "type": "number", "minimum": 0 }, - "intensity_category": { "type": "string" }, - "depth_m": { "type": "number", "minimum": 0 } - }, - "allOf": [ - { "if": { "properties": { "event_type": { "const": "planting" } } }, - "then": { "required": ["leaf_c_kg_m2"] } }, - { "if": { "properties": { "event_type": { "const": "harvest" } } }, - "then": { "required": ["frac_above_removed_0to1"] } }, - { "if": { "properties": { "event_type": { "const": "irrigation" } } }, - "then": { "required": ["amount_mm", "method"] } }, - { "if": { "properties": { "event_type": { "const": "fertilization" } } }, - "then": { "anyOf": [ - { "required": ["org_c_kg_m2"] }, - { "required": ["nh4_n_kg_m2"] }, - { "required": ["no3_n_kg_m2"] } - ] } }, - { "if": { "properties": { "event_type": { "const": "tillage" } } }, - "then": { "required": ["tillage_eff_0to1"] } } - ], - "additionalProperties": true - } + "tillage_eff_0to1": { "type": "number", "minimum": 0 }, + "intensity_category": { "type": "string" }, + "depth_m": { "type": "number", "minimum": 0 } + }, + "allOf": [ + { "if": { "properties": { "event_type": { "const": "planting" } } }, + "then": { "required": ["leaf_c_kg_m2"] } }, + { "if": { "properties": { "event_type": { "const": "harvest" } } }, + "then": { "required": ["frac_above_removed_0to1"] } }, + { "if": { "properties": { "event_type": { "const": "irrigation" } } }, + "then": { "required": ["amount_mm", "method"] } }, + { "if": { "properties": { "event_type": { "const": "fertilization" } } }, + "then": { "anyOf": [ + { "required": ["org_c_kg_m2"] }, + { "required": ["nh4_n_kg_m2"] }, + { "required": ["no3_n_kg_m2"] } + ] } }, + { "if": { "properties": { "event_type": { "const": "tillage" } } }, + "then": { "required": ["tillage_eff_0to1"] } } + ], + "additionalProperties": true + } + } + }, + "additionalProperties": false } - }, - "additionalProperties": false + } } diff --git a/modules/data.land/man/validate_events_json.Rd b/modules/data.land/man/validate_events_json.Rd new file mode 100644 index 00000000000..85120ecd62b --- /dev/null +++ b/modules/data.land/man/validate_events_json.Rd @@ -0,0 +1,35 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/validate_events.R +\name{validate_events_json} +\alias{validate_events_json} +\title{Validate PEcAn events JSON against schema v0.1.0} +\usage{ +validate_events_json(events_json, verbose = TRUE) +} +\arguments{ +\item{events_json}{character. Path to the JSON file to validate.} + +\item{verbose}{logical. When `TRUE`, include detailed AJV messages on error.} +} +\value{ +Logical TRUE if valid (or validator unavailable), FALSE if invalid. +} +\description{ +Validates a PEcAn events JSON file (single-site object or an array of site +objects) against the bundled JSON Schema (draft 2020-12) using the AJV +engine. +} +\details{ +- Logs an error and returns FALSE if the JSON file does not exist or does + not conform to the schema. +- Logs a warning and returns TRUE if the optional package `jsonvalidate` is + not installed, so calling code can proceed without a hard dependency. +} +\examples{ +# validate_events_json(system.file("events_fixtures/events_site1.json", +# package = "PEcAn.data.land")) + +} +\author{ +David LeBauer +} diff --git a/modules/data.land/tests/testthat/test-validate_events_json.R b/modules/data.land/tests/testthat/test-validate_events_json.R new file mode 100644 index 00000000000..723aff3abc2 --- /dev/null +++ b/modules/data.land/tests/testthat/test-validate_events_json.R @@ -0,0 +1,15 @@ +context("validate_events_json") + +testthat::test_that("validate_events_json validates good fixtures", { + f1 <- system.file("events_fixtures/events_site1.json", package = "PEcAn.data.land", mustWork = TRUE) + f2 <- system.file("events_fixtures/events_site1_site2.json", package = "PEcAn.data.land", mustWork = TRUE) + testthat::expect_true(PEcAn.data.land::validate_events_json(f1)) + testthat::expect_true(PEcAn.data.land::validate_events_json(f2)) +}) + +testthat::test_that("validate_events_json returns FALSE on invalid JSON", { + bad <- withr::local_tempfile(fileext = ".json") + # Missing required field: events + jsonlite::write_json(list(pecan_events_version = "0.1.0", site_id = "X"), bad, auto_unbox = TRUE) + testthat::expect_false(PEcAn.data.land::validate_events_json(bad)) +}) From cfd616ab7da97760f3ef5a80aef95f85805fcbd7 Mon Sep 17 00:00:00 2001 From: David LeBauer Date: Wed, 15 Oct 2025 17:28:46 -0700 Subject: [PATCH 23/30] add jsonlite suggests to data.land --- docker/depends/pecan_package_dependencies.csv | 1 + modules/data.land/DESCRIPTION | 1 + 2 files changed, 2 insertions(+) diff --git a/docker/depends/pecan_package_dependencies.csv b/docker/depends/pecan_package_dependencies.csv index 4b626db69a4..290025edc20 100644 --- a/docker/depends/pecan_package_dependencies.csv +++ b/docker/depends/pecan_package_dependencies.csv @@ -134,6 +134,7 @@ "jsonlite","*","models/sipnet","Imports",FALSE "jsonlite","*","models/stics","Imports",FALSE "jsonlite","*","modules/data.atmosphere","Imports",FALSE +"jsonlite","*","modules/data.land","Suggests",FALSE "jsonlite","*","modules/data.remote","Suggests",FALSE "jsonvalidate","*","models/sipnet","Suggests",FALSE "jsonvalidate","*","modules/data.land","Suggests",FALSE diff --git a/modules/data.land/DESCRIPTION b/modules/data.land/DESCRIPTION index 126a3a87f0d..0b6a85345d9 100644 --- a/modules/data.land/DESCRIPTION +++ b/modules/data.land/DESCRIPTION @@ -63,6 +63,7 @@ Imports: Suggests: dataone, datapack, + jsonlite, jsonvalidate, getPass, glue, From 7211cc3567f50085e1d30700aab2d87e4546fce6 Mon Sep 17 00:00:00 2001 From: David LeBauer Date: Wed, 15 Oct 2025 17:57:32 -0700 Subject: [PATCH 24/30] use system.file for test fixtures --- .../tests/testthat/test-write.events.SIPNET.R | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/models/sipnet/tests/testthat/test-write.events.SIPNET.R b/models/sipnet/tests/testthat/test-write.events.SIPNET.R index ec00ada9b8e..f792a1fcc09 100644 --- a/models/sipnet/tests/testthat/test-write.events.SIPNET.R +++ b/models/sipnet/tests/testthat/test-write.events.SIPNET.R @@ -3,16 +3,10 @@ context("write.events.SIPNET") # Helper to remove excess whitespace norm <- function(x) gsub("\\s+", " ", trimws(x)) -# Helper to locate fixtures from PEcAn.data.land, with source fallback -fixture_path <- function(name) { - p <- system.file(file.path("events_fixtures", name), package = "PEcAn.data.land") - if (!is.null(p) && nzchar(p)) return(p) - # Fallback to source path in monorepo when PEcAn.data.land isn't installed - testthat::test_path(file.path("../../../../modules/data.land/inst/events_fixtures", name)) -} - testthat::test_that("write.events.SIPNET produces expected lines", { - ev_json1 <- fixture_path("events_site1.json") + ev_json1 <- system.file(file.path("events_fixtures", "events_site1.json"), + package = "PEcAn.data.land" + ) outdir <- withr::local_tempdir() files <- write.events.SIPNET(ev_json1, outdir) expect_length(files, 1) @@ -30,7 +24,9 @@ testthat::test_that("write.events.SIPNET produces expected lines", { }) testthat::test_that("write.events.SIPNET handles multi-site events.json (one file per site)", { - ev_json2 <- fixture_path("events_site1_site2.json") + ev_json2 <- system.file(file.path("events_fixtures", "events_site1_site2.json"), + package = "PEcAn.data.land" + ) outdir <- withr::local_tempdir() files <- write.events.SIPNET(ev_json2, outdir) testthat::expect_length(files, 2) From 338db7687453690cd86423ada93a73f958917fe1 Mon Sep 17 00:00:00 2001 From: David LeBauer Date: Mon, 20 Oct 2025 13:20:04 -0700 Subject: [PATCH 25/30] return NA if no validator Co-authored-by: Chris Black --- modules/data.land/R/validate_events.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/data.land/R/validate_events.R b/modules/data.land/R/validate_events.R index 1b550f2a707..620e400e8b9 100644 --- a/modules/data.land/R/validate_events.R +++ b/modules/data.land/R/validate_events.R @@ -29,7 +29,7 @@ validate_events_json <- function(events_json, verbose = TRUE) { if (!requireNamespace("jsonvalidate", quietly = TRUE)) { PEcAn.logger::logger.warn("Skipping events schema validation: package 'jsonvalidate' not installed.") - return(TRUE) + return(NA) } schema <- system.file("events_schema_v0.1.0.json", package = "PEcAn.data.land", mustWork = TRUE) From 79c17d8e73cb6e2f5038ea02f79d80148e1f75bc Mon Sep 17 00:00:00 2001 From: David LeBauer Date: Mon, 20 Oct 2025 13:20:37 -0700 Subject: [PATCH 26/30] Update models/sipnet/DESCRIPTION Co-authored-by: Chris Black --- models/sipnet/DESCRIPTION | 1 - 1 file changed, 1 deletion(-) diff --git a/models/sipnet/DESCRIPTION b/models/sipnet/DESCRIPTION index 2ad29266581..16dd9ff3447 100644 --- a/models/sipnet/DESCRIPTION +++ b/models/sipnet/DESCRIPTION @@ -26,7 +26,6 @@ Imports: stats Suggests: coda, - jsonvalidate, testthat (>= 1.0.2), withr SystemRequirements: SIPNET ecosystem model From b84ef76794b5ecd18bf4cde2718bfb15080ab0c9 Mon Sep 17 00:00:00 2001 From: David LeBauer Date: Mon, 20 Oct 2025 13:20:55 -0700 Subject: [PATCH 27/30] Update docker/depends/pecan_package_dependencies.csv Co-authored-by: Chris Black --- docker/depends/pecan_package_dependencies.csv | 1 - 1 file changed, 1 deletion(-) diff --git a/docker/depends/pecan_package_dependencies.csv b/docker/depends/pecan_package_dependencies.csv index 290025edc20..0a9d67c7976 100644 --- a/docker/depends/pecan_package_dependencies.csv +++ b/docker/depends/pecan_package_dependencies.csv @@ -136,7 +136,6 @@ "jsonlite","*","modules/data.atmosphere","Imports",FALSE "jsonlite","*","modules/data.land","Suggests",FALSE "jsonlite","*","modules/data.remote","Suggests",FALSE -"jsonvalidate","*","models/sipnet","Suggests",FALSE "jsonvalidate","*","modules/data.land","Suggests",FALSE "keras3",">= 1.0.0","modules/assim.sequential","Suggests",FALSE "knitr","*","base/visualization","Suggests",FALSE From f7761a85cb97a24823095fdbbf0692bd8b9310fa Mon Sep 17 00:00:00 2001 From: David LeBauer Date: Mon, 20 Oct 2025 13:59:31 -0700 Subject: [PATCH 28/30] - require testthat >= 3.1.0 to support new mocking features in tests (3.1.0 was released in 2021) - add test that validate_events_json return NA if jsonvalidate not installed --- modules/data.land/DESCRIPTION | 2 +- modules/data.land/R/validate_events.R | 3 ++- .../tests/testthat/test-validate_events_json.R | 17 +++++++++++++++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/modules/data.land/DESCRIPTION b/modules/data.land/DESCRIPTION index 0b6a85345d9..52794ca0ad7 100644 --- a/modules/data.land/DESCRIPTION +++ b/modules/data.land/DESCRIPTION @@ -71,7 +71,7 @@ Suggests: redland, raster, reticulate, - testthat (>= 1.0.2), + testthat (>= 3.1.0), withr, MASS Remotes: diff --git a/modules/data.land/R/validate_events.R b/modules/data.land/R/validate_events.R index 620e400e8b9..95ebd486960 100644 --- a/modules/data.land/R/validate_events.R +++ b/modules/data.land/R/validate_events.R @@ -12,7 +12,8 @@ #' @param events_json character. Path to the JSON file to validate. #' @param verbose logical. When `TRUE`, include detailed AJV messages on error. #' -#' @return Logical TRUE if valid (or validator unavailable), FALSE if invalid. +#' @return Logical TRUE if valid, FALSE if invalid. +#' NA if validator unavailable. #' #' @author David LeBauer #' diff --git a/modules/data.land/tests/testthat/test-validate_events_json.R b/modules/data.land/tests/testthat/test-validate_events_json.R index 723aff3abc2..9522aa29101 100644 --- a/modules/data.land/tests/testthat/test-validate_events_json.R +++ b/modules/data.land/tests/testthat/test-validate_events_json.R @@ -13,3 +13,20 @@ testthat::test_that("validate_events_json returns FALSE on invalid JSON", { jsonlite::write_json(list(pecan_events_version = "0.1.0", site_id = "X"), bad, auto_unbox = TRUE) testthat::expect_false(PEcAn.data.land::validate_events_json(bad)) }) + +testthat::test_that("validate_events_json returns NA if jsonvalidate is unavailable", { + f1 <- system.file("events_fixtures/events_site1.json", package = "PEcAn.data.land", mustWork = TRUE) + # Use testthat mocking to simulate missing jsonvalidate pkg by overriding base::requireNamespace + testthat::with_mocked_bindings( + requireNamespace = function(pkg, quietly = TRUE) { + if (identical(pkg, "jsonvalidate")) { + return(FALSE) + } + base::requireNamespace(pkg, quietly = quietly) + }, + { + testthat::expect_true(is.na(PEcAn.data.land::validate_events_json(f1))) + }, + .package = "base" + ) +}) From 8e3bd1ab3f6a20fe87535cbc44a590272044358d Mon Sep 17 00:00:00 2001 From: David LeBauer Date: Mon, 20 Oct 2025 14:08:58 -0700 Subject: [PATCH 29/30] Update CHANGELOG and NEWS --- CHANGELOG.md | 4 +++- modules/data.land/NEWS.md | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2feadaa9b5e..45e4ff55674 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,9 @@ For more information about this file see also [Keep a Changelog](http://keepacha - Added `AmeriFlux_met_ensemble()` function with ERA5 fallback for AmeriFlux meteorological data processing and ensemble generation - 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. - Added parallel mode for the entire SDA workflow. -- `write.events.SIPNET()` to generate SIPNET `events.in` files from a `events.json` file. +- Define, add support for, and parse events schema + - Events schema and validate_events() function to PEcAn.data.land (#3623, #3521) + - Add `write.events.SIPNET()` to generate SIPNET `events.in` files from a `events.json` file. - Included all relevant carbon pools (`ROOT_BIOMASS`, `AG_BIOMASS`, `SOIL_STOCK`, `LIT_BIOMASS`) in BADM-based IC extraction; excluded non-pool variables like `SOIL_CHEM`. - Added explicit support for `LIT_BIOMASS` to fully utilize **BADM** biomass capabilities. - Added `test-IC_BADM_Utilities.R` to validate BADM initial condition extraction and processing diff --git a/modules/data.land/NEWS.md b/modules/data.land/NEWS.md index 425008658af..f89d5865de4 100644 --- a/modules/data.land/NEWS.md +++ b/modules/data.land/NEWS.md @@ -5,6 +5,7 @@ - **`soilgrids_ic_process`**: A function to extract, process, and generate ensemble members from SoilGrids250m data. - **`preprocess_soilgrids_data`**: A helper function to handle missing values and ensure data integrity during preprocessing. - **`generate_soilgrids_ensemble`**: A function to create ensemble members for a site based on processed soil carbon data. +- Add events schema and validate_events() function to validate events.json files against the schema (#3623, #3521). # PEcAn.data.land 1.8.2 - Removed unused parameter `machine` from put_veg_module() From 579942b366f984b381d473cf17727c5b0e4d9f19 Mon Sep 17 00:00:00 2001 From: David LeBauer Date: Mon, 20 Oct 2025 14:13:46 -0700 Subject: [PATCH 30/30] Update testthat version requirement and documentation for validate_events_json --- docker/depends/pecan_package_dependencies.csv | 2 +- modules/data.land/man/validate_events_json.Rd | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docker/depends/pecan_package_dependencies.csv b/docker/depends/pecan_package_dependencies.csv index 0a9d67c7976..d5f37b06e26 100644 --- a/docker/depends/pecan_package_dependencies.csv +++ b/docker/depends/pecan_package_dependencies.csv @@ -623,7 +623,6 @@ "testthat",">= 1.0.2","models/template","Suggests",FALSE "testthat",">= 1.0.2","modules/allometry","Suggests",FALSE "testthat",">= 1.0.2","modules/assim.batch","Suggests",FALSE -"testthat",">= 1.0.2","modules/data.land","Suggests",FALSE "testthat",">= 1.0.2","modules/data.remote","Suggests",FALSE "testthat",">= 1.0.2","modules/meta.analysis","Suggests",FALSE "testthat",">= 1.0.2","modules/rtm","Suggests",FALSE @@ -635,6 +634,7 @@ "testthat",">= 2.0.0","modules/benchmark","Suggests",FALSE "testthat",">= 3.0.0","models/sibcasa","Suggests",FALSE "testthat",">= 3.0.4","base/qaqc","Suggests",FALSE +"testthat",">= 3.1.0","modules/data.land","Suggests",FALSE "testthat",">= 3.1.7","modules/data.atmosphere","Suggests",FALSE "tibble","*","base/db","Imports",FALSE "tibble","*","models/ed","Imports",FALSE diff --git a/modules/data.land/man/validate_events_json.Rd b/modules/data.land/man/validate_events_json.Rd index 85120ecd62b..bab649c3a3c 100644 --- a/modules/data.land/man/validate_events_json.Rd +++ b/modules/data.land/man/validate_events_json.Rd @@ -12,7 +12,8 @@ validate_events_json(events_json, verbose = TRUE) \item{verbose}{logical. When `TRUE`, include detailed AJV messages on error.} } \value{ -Logical TRUE if valid (or validator unavailable), FALSE if invalid. +Logical TRUE if valid, FALSE if invalid. +NA if validator unavailable. } \description{ Validates a PEcAn events JSON file (single-site object or an array of site