Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
9fe0165
first draft of fn to parse restart dates out of event files
infotroph Feb 23, 2026
4708176
Update modules/data.land/tests/testthat/test-events_to_crop_cycle_sta…
infotroph Feb 27, 2026
d8df7d0
require crop id in planting and harvest; add docs on event properties
infotroph Feb 25, 2026
24db912
right, we said optional for harvest
infotroph Feb 25, 2026
7d145c8
changelog
infotroph Feb 25, 2026
67e055d
WIP changes
ashiklom Mar 27, 2026
dac5a9e
split prepare events from run workflow
ashiklom Mar 27, 2026
175f15a
wip changes
ashiklom Mar 27, 2026
a97a946
remove .Renviron
ashiklom Mar 27, 2026
4174200
sipnet output workaround
ashiklom Mar 27, 2026
236fb4a
pixi dependencies
ashiklom Mar 27, 2026
b093d75
crop --> crop_code for parsing crop cycles
ashiklom Mar 27, 2026
c47817c
working run-sipnet workflow
ashiklom Mar 27, 2026
4bf352f
add cdo and r-stars to pixi environment
ashiklom Apr 3, 2026
fc55b75
workflow runs with outputs concatenated
ashiklom Apr 3, 2026
c33de6c
fix syntax for model restarts
ashiklom Apr 3, 2026
a68f8cd
working restart workflow
ashiklom Apr 3, 2026
964b174
WIP PFT configuration
ashiklom Apr 4, 2026
0cf8782
ensemble restart workflow
ashiklom Apr 5, 2026
3cc9171
move to root workflows dir
ashiklom Apr 5, 2026
5e8a404
refactor settings creation into own file
ashiklom Apr 5, 2026
f0b07c7
pull run_sipnet_segmented into own function
ashiklom Apr 5, 2026
0df2bbc
update plotting code
ashiklom Apr 5, 2026
2b4bf9e
support event ensembles
ashiklom Apr 5, 2026
e3cf5fb
add README
ashiklom Apr 5, 2026
1505297
sipnet: make sure trait.values is a list
ashiklom Apr 9, 2026
f33d822
sipnet: coerce start/end time to datetime
ashiklom Apr 9, 2026
0e4cb63
use real harvest events
ashiklom Apr 9, 2026
d155df7
allow prepare-events to work without irrigation
ashiklom Apr 9, 2026
cb5cc1c
update met paths to use xxN_yy.yE -type naming
ashiklom Apr 9, 2026
22bc879
put all settings logic into prepare-settings
ashiklom Apr 9, 2026
d1d8252
better plotting
ashiklom Apr 9, 2026
4f9781c
bugfix handling of trait.values
ashiklom Apr 9, 2026
2c9521d
pixi updates + script to find winter crops
ashiklom Apr 9, 2026
959ed7b
better variable list handling in plots
ashiklom Apr 9, 2026
f2d384d
fix start/end handling & interaction with segments
ashiklom Apr 9, 2026
9a378cb
simplify crew config
ashiklom Apr 10, 2026
67b1039
add `combine_sipnet_out`; document new split funcs
ashiklom Apr 10, 2026
df8f712
write new job.sh instead of actually doing runs
ashiklom Apr 10, 2026
b7fac24
bugfix model2netcdf.sipnet
ashiklom Apr 10, 2026
546ee44
complete more pecanic workflow
ashiklom Apr 10, 2026
c0317a4
grepv --> grep(..., values = TRUE)
ashiklom Apr 11, 2026
390e5d8
bugfix: seq_along --> seq_len
ashiklom Apr 11, 2026
ea4ca66
typo extra file.path
ashiklom Apr 11, 2026
dc4e8bb
More robust segment logic
ashiklom Apr 17, 2026
8ab19bd
crop -> crop_code in tests
ashiklom Apr 17, 2026
7f03d04
outpath should be NULL, not FALSE
ashiklom Apr 17, 2026
10eec01
better docs + warnings for sipnet trait.values
ashiklom Apr 17, 2026
c31c63b
comment about file sorting
ashiklom Apr 17, 2026
d6555db
warn on non-alternating harvest-planting cycles
ashiklom Apr 17, 2026
2173db9
remove pixi and WIP files
ashiklom Apr 17, 2026
11fa863
better combine_sipnet_out docs
ashiklom Apr 17, 2026
2ce339e
add todo for crop2pft
ashiklom Apr 17, 2026
3843f12
option to force reruns of segments (default=FALSE)
ashiklom Apr 17, 2026
2756786
set pft_dir for BU SCC
ashiklom Apr 17, 2026
61aa302
Merge branch 'develop' into sipnet-restart-workflow
ashiklom Apr 17, 2026
f720c3d
Merge remote-tracking branch 'origin/develop' into sipnet-restart-wor…
ashiklom Apr 17, 2026
3ea3b2b
drop "sipnet only takes one PFT" from warning
ashiklom Apr 17, 2026
53f2516
update sipnet configs docs
ashiklom Apr 17, 2026
0b3ac45
split inputs consistently is < end.time, not <=
ashiklom Apr 17, 2026
3ab8def
downgrade datetime coercion message to debug
ashiklom Apr 17, 2026
691e7d5
change inputs -> results to avoid confusion
ashiklom Apr 17, 2026
617b2fe
various R CMD check fixes
ashiklom Apr 17, 2026
e49e7e8
fix split inputs tests with new structure
ashiklom Apr 17, 2026
79952c4
fix events_to_crop_cycle_starts tests
ashiklom Apr 17, 2026
3f6ea0d
fix sipnet split_input tests
ashiklom Apr 17, 2026
464acf7
more rcheck fixes
ashiklom Apr 17, 2026
7456de0
revert setting default ensemble method
ashiklom Apr 17, 2026
a40d0b2
add tests for splitting sipnet events
ashiklom Apr 17, 2026
0abbbf7
fix: write_events.SIPNET actually sorts by date
ashiklom Apr 17, 2026
f061e09
continue runs to end of year
ashiklom Apr 17, 2026
6554a6b
!(length > 0) --> length == 0
ashiklom Apr 17, 2026
d5c359e
run in whole-year chunks
ashiklom Apr 17, 2026
453fc3b
generate dependencies (unrelated - data.atmosphere)
ashiklom Apr 17, 2026
7702281
remove warning for duplicate harvest/planting
ashiklom Apr 17, 2026
a5dae48
Revert temp changes to data.atmosphere DESCRIPTION
ashiklom Apr 17, 2026
81e49dc
.data predicate in crop cycle starts
ashiklom Apr 17, 2026
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ For more information about this file see also [Keep a Changelog](http://keepacha
`MCMCpack`, `mvtnorm`, `neonUtilities`, `neonstore`, `PEcAn.benchmark`,
`PEcAn.visualization`, `rjags`, `sirt`, and `sp` from `Imports` to
`Suggests` (@omkarrr2533, #3599).
- Management events specified via `events.json` are now required to specify a crop code for each planting event, so that models can know when to restart with a different PFT (#3828, #3836).



Expand Down
4 changes: 2 additions & 2 deletions base/remote/R/check_model_run.R
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ check_model_run <- function(out, stop.on.error = TRUE) {
success <- FALSE
msg <- paste0("Model run aborted with the following error:\n", out)
if (stop.on.error) {
PEcAn.logger::logger.severe(msg)
PEcAn.logger::logger.severe(msg, wrap = FALSE)
} else {
PEcAn.logger::logger.error(msg)
PEcAn.logger::logger.error(msg, wrap = FALSE)
}
} else {
success <- TRUE
Expand Down
80 changes: 80 additions & 0 deletions book_source/03_topical_pages/02_pecan_standards.Rmd
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,86 @@ See the [Soil Data] section on more into on creating a standard soil data file.

See the [Vegetation Data] section on more info on creating a standard vegetation data file

### Management events

Externally imposed changes in system state, such as planting and harvest or tillage of crop systems, are represented as a sequence of "events" that are each defined by a date, event type, and one or more event-specific properties that describe the kind and magnitude of change imposed. PEcAn passes the same representation of a given event to all models, but interpretation of all properties -- including dates and event types -- is necessarily model-specific and could include ignoring an event entirely.

Event types are defined by the PEcAn events spec, which is currently provided as a JSON schema file bundled into the PEcAn.data.land package (TODO consider exposing this in a more linkable format?). Valid events files are stored as JSON, with each events file containing an array of sites. Each site contains an id, a schema version, and an array of events, usually recorded in date order. Each event has a date, an event type, and one or more values from the required and optional properties that are documented below for each event type. Any event is also allowed to contain arbitrary additional properties not mentioned in the spec; PEcAn will pass these on to models with no further validation.

The currently supported event types are: planting, harvest, irrigation, fertilization, and tillage. Each of these is described briefly below. You will note that all of them are focused on agronomic management of croplands; future work will extend this framework to include disturbance of unmanaged systems such as fire, inundation or insect outbreaks.


#### planting

Addition of live biomass that is expected to grow according to the model's plant growth rules. The current spec expects a "planting" to be a transplantation of leafed-out seedlings; models which represent planting as addition of unsprouted seeds might choose to adjust the date to account for germination time.

Required properties:

* `crop_code`: a machine-readable identifier that specifies what type of plant was added. Should preferably be short and use only characters that are safe in filenames.
* `leaf_c_kg_m2`: Amount of leaf biomass added.

Optional properties:

* `wood_c_kg_m2`: Biomass added as aboveground stems. Assumed zero if not specified.
* `fine_root_c_kg_m2`: Biomass added to belowground roots. Assumed zero if not specified.
* `cultivar`: Identifier for finer-grainer plant type identifiers that will be meaningful to models with detailed growth parameterizations but likely ignored by PFT-based models.
* `crop_display`: A human-readable name for the added plant type.

#### harvest

Removal of biomass from the living pool, either by transferring it to dead (litter) biomass or by removing it from the system. Specified as a fraction of live biomass.

Required properties:
* `frac_above_removed_0to1`: Fraction of existing aboveground biomass removed from the system.

Optional properties:

* `frac_below_removed_0to1`: Fraction of existing beloweground biomass removed from the system. Assumed zero if not specified.
* `frac_above_to_litter_0to1`, `frac_below_to_litter_0to1`: fraction of existing above- or below-ground live biomass converted to dead litter. Assumed zero if not specified.
* `crop_code`: A machine_readable identifier that specifies what type of plant was harvested.
* `cultivar`: Identifier for finer-grainer plant type identifiers that will be meaningful to models with detailed growth parameterizations but likely ignored by PFT-based models.
* `crop_display`: A human-readable name for the added plant type.

#### irrigation

Addition of water to the system beyond that specified as precipitation in met files. Note that only the total amount of water is specified and not the application rate; assumptions about the duration of the event will be model-specific.

Required properties:

* `method`: How water was applied. One of "soil", "canopy", "flood".
* `amount_mm`: Amount of water applied.

Optional properties:

* `immed_evap_frac_0to1`: fraction of the applied water that evaporates before reaching the soil water pool.

#### fertilization

Addition of exogenous nutrients, including non-crop carbon (e.g. manure, compost). Nutrients other than N and C are not currently included but may be added in future schemas.

Required properties:

* at least one of `org_c_kg_m2`, `nh4_n_kg_m2`, `no3_n_kg_m2`, which are respectively the amount of organic C, ammonium N, and nitrate N added.

Optional properties:

* any of `org_c_kg_m2`, `nh4_n_kg_m2`, `no3_n_kg_m2` not already counted as required. All are assumed zero if not specified.
* `org_n_kg_m2`: the amount of N added in organic compounds. Assumed zero if not specified. If specifying, you probably want to specify `org_c_kg_m2` as well but the schema does not enforce this.

#### tillage

Physical disturbance of the soil and litter pools. Models differ greatly in their representation of tillage effects and this spec intentionally limits its representation to a simple intensity score whose maximum should be interpreted as "the most complete possible mixing/disturbance." Whether this is sufficient detail will be re-evaluated in future revisions.

Required properties:

* `tillage_eff_0to1`: Relative tillage intensity, with 0 meaning no disturbance at all and 1 meaning the most complete disturbance possible.

Optional properties:

* `intensity_category`: string giving additional information on the type or amount of tillage performed.
* `depth_m`: Depth of soil affected by the event.


## Output Standards

* created by `model2netcdf` functions
Expand Down
1 change: 1 addition & 0 deletions models/sipnet/NAMESPACE
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Generated by roxygen2: do not edit by hand

export(combine_sipnet_out)
export(mergeNC)
export(met2model.SIPNET)
export(model2netcdf.SIPNET)
Expand Down
36 changes: 36 additions & 0 deletions models/sipnet/R/combine_sipnet_out.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#!/usr/bin/env Rscript

#' Combine a bunch of `sipnet.out` files into a single file
#'
#' @param directory Parent directory to search for `sipnet.out` files. You must
#' provide either this or an explicit vector of `files`.
#' @param outfile File to which combined `sipnet.out` will be written
#' @param files Optional vector of paths to files to combine. All must be
#' readable with [read_sipnet_out()]. If `NULL` (default), looks for files
#' named `sipnet.out` in `directory` and its subdirectories, recursively.
#'
#' @return `outfile` (path to output file), invisibly
#' @export
combine_sipnet_out <- function(directory, outfile, files = NULL) {
if (missing(directory) && is.null(files)) {
PEcAn.logger::logger.severe("Must provide either `directory` or `files`")
}
if (is.null(files)) {
# NOTE that this expects file paths (including parent directories) to be
# lexicographically sorted. For the common case of segmented SIPNET runs,
# the parent directories are named `segment_001`, `segment_002`, etc., so
# this will work automatically.
# If you don't want to make this assumption, or have a custom sort order,
# pass `files` directly to this function.
files <- sort(list.files(directory, "sipnet\\.out", full.names = TRUE, recursive = TRUE))
Comment thread
ashiklom marked this conversation as resolved.
}
if (length(files) == 0) {
PEcAn.logger::logger.severe("No files provided; nothing to combine.")
}
flist <- lapply(files, read_sipnet_out)
combined <- do.call(rbind.data.frame, flist)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential FUTURE enhancement: Provide an option to add a column with a filename or other source identifier, so that we could pipe output from directories full of sipnet.out straight into a multi-site/run netCDF without needing to write the plaintext back out in between.

# Mimic the SIPNET fixed-width right-aligned format
combined_fwf <- format(combined, justify = "right")
utils::write.table(combined_fwf, outfile, row.names = FALSE, col.names = TRUE, quote = FALSE, sep = " ")
invisible(outfile)
}
34 changes: 2 additions & 32 deletions models/sipnet/R/model2netcdf.SIPNET.R
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#' }
#' @export mergeNC
#' @name mergeNC
#' @importFrom rlang .data
#' @source https://github.com/RS-eco/processNC/blob/main/R/mergeNC.R
mergeNC <- function(
##title<< Aggregate data in netCDF files
Expand Down Expand Up @@ -61,38 +62,7 @@ model2netcdf.SIPNET <- function(outdir, sitelat, sitelon, start_date, end_date,
overwrite = FALSE, conflict = FALSE) {
### Read in model output in SIPNET format
sipnet_out_file <- file.path(outdir, prefix)
# SIPNET v1 had a "Notes" comment line before the header; v2 removed it.
# if the first line starts with "year", there is no Notes line.
first_line <- readLines(sipnet_out_file, n = 1)
skip_n <- if (grepl("^year", first_line)) 0 else 1
# Temporary workaround until
# https://github.com/PecanProject/sipnet/issues/304 is resolved.
sipnet_output <- tryCatch({
utils::read.table(sipnet_out_file, header = TRUE, skip = skip_n, sep = "")
}, error = function(err) {
PEcAn.logger::logger.warn(
"Failed to read using `read.table`. ",
"Trying to parse output manually."
)
raw_lines <- readLines(sipnet_out_file)
raw_header <- raw_lines[[1 + skip_n]]
raw_body <- utils::tail(raw_lines, -(1 + skip_n))
# SIPNET output is right-aligned with the column names in the header.
# We use this to figure out where the numbers end if there are no spaces.
token_matches <- gregexpr("\\S+", raw_header, perl = TRUE)
proc_header <- regmatches(raw_header, token_matches)[[1]]
col_ends <- token_matches[[1]] + attr(token_matches[[1]], "match.length") - 1
col_starts <- c(1, utils::head(col_ends, -1) + 1)
col_widths <- col_ends - col_starts + 1
result <- utils::read.fwf(
textConnection(raw_body),
widths = col_widths,
col.names = proc_header,
na.strings = c("nan", "-nan")
)
result[] <- lapply(result, as.numeric)
result
})
sipnet_output <- read_sipnet_out(sipnet_out_file)
#sipnet_output_dims <- dim(sipnet_output)

### Determine number of years and output timestep
Expand Down
40 changes: 40 additions & 0 deletions models/sipnet/R/read_sipnet_out.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#' Read `sipnet.out` file to `data.frame`
#'
#' @param sipnet_out_file Path to `sipnet.out` file
#'
#' @return `data.frame` of SIPNET output
read_sipnet_out <- function(sipnet_out_file) {
# SIPNET v1 had a "Notes" comment line before the header; v2 removed it.
# if the first line starts with "year", there is no Notes line.
first_line <- readLines(sipnet_out_file, n = 1)
skip_n <- if (grepl("^year", first_line)) 0 else 1
# Temporary workaround until
# https://github.com/PecanProject/sipnet/issues/304 is resolved.
sipnet_output <- tryCatch({
utils::read.table(sipnet_out_file, header = TRUE, skip = skip_n, sep = "")
}, error = function(err) {
PEcAn.logger::logger.warn(
"Failed to read using `read.table`. ",
"Trying to parse output manually."
)
raw_lines <- readLines(sipnet_out_file)
raw_header <- raw_lines[[1 + skip_n]]
raw_body <- utils::tail(raw_lines, -(1 + skip_n))
# SIPNET output is right-aligned with the column names in the header.
# We use this to figure out where the numbers end if there are no spaces.
token_matches <- gregexpr("\\S+", raw_header, perl = TRUE)
proc_header <- regmatches(raw_header, token_matches)[[1]]
col_ends <- token_matches[[1]] + attr(token_matches[[1]], "match.length") - 1
col_starts <- c(1, utils::head(col_ends, -1) + 1)
col_widths <- col_ends - col_starts + 1
result <- utils::read.fwf(
textConnection(raw_body),
widths = col_widths,
col.names = proc_header,
na.strings = c("nan", "-nan")
)
result[] <- lapply(result, as.numeric)
result
})
sipnet_output
}
Loading
Loading