Skip to content

Sipnet v2 support#3813

Merged
dlebauer merged 35 commits intoPecanProject:developfrom
infotroph:sipnet-v2
Mar 10, 2026
Merged

Sipnet v2 support#3813
dlebauer merged 35 commits intoPecanProject:developfrom
infotroph:sipnet-v2

Conversation

@infotroph
Copy link
Copy Markdown
Member

@infotroph infotroph commented Feb 14, 2026

Description

Pushing some long-stashed changes so we can iterate on them. NOT YET TESTED.

Implemented so far:

  • write clim files in the 12-column vs format by default
  • switch on version when choosing a template for sipnet.in and sipnet.param

Before merging, need to at least:

  • add CH4 and N2O to model2netcdf
  • update and extend unit tests

And probably also:

  • check for simplifications made possible by the param file being name/value pairs instead of multicolumn
  • update write_restart/read_restart
  • Implement changing command-line switches from settings (esp. turning off GDD)

Motivation and Context

Review Time Estimate

  • Immediately
  • Within one week
  • When possible

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist:

  • My change requires a change to the documentation.
  • My name is in the list of CITATION.cff
  • I agree that PEcAn Project may distribute my contribution under any or all of
    • the same license as the existing code,
    • and/or the BSD 3-clause license.
  • I have updated the CHANGELOG.md.
  • I have updated the documentation accordingly.
  • I have read the CONTRIBUTING document.
  • I have added tests to cover my changes.
  • All new and existing tests passed.

Comment thread models/sipnet/inst/template.param_v2
Comment thread models/sipnet/R/met2model.SIPNET.R Outdated
@github-actions github-actions Bot added the tests label Mar 4, 2026
@divine7022
Copy link
Copy Markdown
Member

divine7022 commented Mar 4, 2026

picking up from Chris's initial v2 scaffolding , the main gaps were that the v2 templates were incomplete and model2netcdf couldn't parse v2 output. Here's what this round adds:

template.param_v2 -- stripped the 22 params that no longer exist in v2 (microbeInit, qualityLeaf, cn_ prefixed ones, etc.) and added the 16 N-cycle/anaerobic/CH4 params that initializeOneModelParam() now expects. All 73 params verified against sipnet.c

sipnet.in_v2 -- added the six runtime flags (NITROGEN_CYCLE, LITTER_POOL, ANAEROBIC, GROWTH_RESP, WATER_HRESP, LEAF_WATER) that replaced the old compile time #ifdefs in v2

write.config.SIPNET -- flag injection block so users can toggle N-cycle/litter/anaerobic from the XML settings. Also added guards so v1 only params (litterWHC, litWaterDrainRate, litterWFracInit, microbeInit, m_ballBerry) don't get written into v2 configs where they would be unknown

model2netcdf.SIPNET -- v2 dropped the "Notes:" line before the header row, so the old skip=1 broke on v2 output. Now auto detects the format. Also handles missing litterWater column (LITTER_WATER flag removed in v2) and converts the 8 new N/CH4 output columns to NetCDF (mineral_N, soil_organic_N, litter_N, N2O, N_leaching, N_fixation, N_uptake, CH4)

added tests for v1 and v2 format coverage, including N2O/CH4 extraction and the missing litterWater edge case.

v1 configs still work exactly as before when <revision> is unset or 1.0; and set 2.0 for v2 support

@divine7022 divine7022 requested a review from dlebauer March 4, 2026 04:53
Copy link
Copy Markdown
Member

@dlebauer dlebauer left a comment

Choose a reason for hiding this comment

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

Thanks for getting this started @infotroph and for bringing this home @divine7022

I left a few questions & comments, none critical; most are thoughts to consider in future iterations.

Comment thread models/sipnet/R/met2model.SIPNET.R Outdated
Comment thread models/sipnet/R/met2model.SIPNET.R Outdated
Comment thread models/sipnet/R/met2model.SIPNET.R Outdated
Comment thread models/sipnet/R/met2model.SIPNET.R Outdated
Comment thread models/sipnet/R/write.configs.SIPNET.R Outdated
write.config.SIPNET <- function(defaults, trait.values, settings, run.id, inputs = NULL, IC = NULL,
restart = NULL, spinup = NULL) {

sipnet_version <- package_version(settings$model$revision, strict = FALSE)
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.

Isn't there an output file that already provides version information? Seems like that would be a more reliable source of version information - at least for >= v2

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.

good idea in principle, sipnet v2 now has a version.h that has version "2.0.0" (and optionally a git hash) into the binary);
but major limitation would be write.config runs pre execution so there's no output to inspect yet, but that querying the binary's version string could be a future enhancement. Worth noting that sipnet v2 binary does support --version output, so this could be added as a validation step

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

There are no outputs at this stage -- we have a binary path but haven't tried calling it yet, and conceptually Sipnet might not even exist on the machine writing the configs. Seems simpler to me to have write.configs do what the settings file says rather than try to infer it from any other source.

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.

haven’t pushed my local changes for this yet, I am commenting here since the context flows -- 2d7559c

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.

heads up:
https://github.com/PecanProject/pecan/actions/runs/22737237619/job/65941705561?pr=3813

fixed version parsing to handle non standard DB revisions like "136", "unk", "git", now only recognizes major versions 1 and 2 as valid sipnet versions; everything else defaults to v1

f99032d

lp <- as.integer(settings$model$litter_pool %||% 1)
an <- as.integer(settings$model$anaerobic %||% 1)
# NITROGEN_CYCLE requires both LITTER_POOL and ANAEROBIC
if (nc == 1 && !(lp == 1 && an == 1)) {
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.

This is enforced by sipnet. In one hand, catching it early makes sense. On the other, it simplifies the pecan code if we delegate consistency checks to sipnet

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.

agreed on the tradeoff.; keeping it for now because it gives clearer error messages before job submission, but noting it's duplicated from sipnet's validateContext()

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

👍 To delegating to Sipnet here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

And while we're looking at this section:

  • How do we feel about the current named options (<model><nitrogen_cycle>0</nitrogen_cycle>...) vs the approach PEcAn.ed2 takes of passing a literal string of command flags (<model><binary_args>--no-nitrogen-cycle</binary_args>...)? I think I prefer the named options, but seemed worth asking.
  • If using named options, would it make sense to accept them as a block and pass them all without needing to enumerate? something like
<model>
  <options>
    <NITROGEN_CYCLE>1</NITROGEN_CYCLE>
    <GDD>0</GDD>
    <NEW_FLAG_SIPNET_HAS_ADDED_SINCE_LAST_REVISION_OF_WRITE.CONFIGS_CODE>1</NEW_FLAG_...>
    ...
  </options>
  ...

...which could then be processed as flag_lines <- paste(names(settings$model$options), "=", settings$model$options, collapse = "\n")?

If this looks good I'll implement it.

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.

this is a good design decision, since new SIPNET flags don't require code changes to write.config;
and for me it seems like could be deferred to follow up PR but shouldn't be mixed into the current one (it changes the XML schema and requires documentation)
make sense ?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

deferred to follow up PR

Fine with me

changes the XML schema

No more than this PR does already -- we're already going from not expecting any options in the XML to expecting them directly inside model. But it's probably worth a review of how other models do this so that we can keep some uniformity.

Comment thread models/sipnet/R/write.configs.SIPNET.R
Comment thread models/sipnet/R/write.configs.SIPNET.R Outdated
write.config.SIPNET <- function(defaults, trait.values, settings, run.id, inputs = NULL, IC = NULL,
restart = NULL, spinup = NULL) {

sipnet_version <- package_version(settings$model$revision, strict = FALSE)
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

There are no outputs at this stage -- we have a binary path but haven't tried calling it yet, and conceptually Sipnet might not even exist on the machine writing the configs. Seems simpler to me to have write.configs do what the settings file says rather than try to infer it from any other source.

lp <- as.integer(settings$model$litter_pool %||% 1)
an <- as.integer(settings$model$anaerobic %||% 1)
# NITROGEN_CYCLE requires both LITTER_POOL and ANAEROBIC
if (nc == 1 && !(lp == 1 && an == 1)) {
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

👍 To delegating to Sipnet here.

lp <- as.integer(settings$model$litter_pool %||% 1)
an <- as.integer(settings$model$anaerobic %||% 1)
# NITROGEN_CYCLE requires both LITTER_POOL and ANAEROBIC
if (nc == 1 && !(lp == 1 && an == 1)) {
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

And while we're looking at this section:

  • How do we feel about the current named options (<model><nitrogen_cycle>0</nitrogen_cycle>...) vs the approach PEcAn.ed2 takes of passing a literal string of command flags (<model><binary_args>--no-nitrogen-cycle</binary_args>...)? I think I prefer the named options, but seemed worth asking.
  • If using named options, would it make sense to accept them as a block and pass them all without needing to enumerate? something like
<model>
  <options>
    <NITROGEN_CYCLE>1</NITROGEN_CYCLE>
    <GDD>0</GDD>
    <NEW_FLAG_SIPNET_HAS_ADDED_SINCE_LAST_REVISION_OF_WRITE.CONFIGS_CODE>1</NEW_FLAG_...>
    ...
  </options>
  ...

...which could then be processed as flag_lines <- paste(names(settings$model$options), "=", settings$model$options, collapse = "\n")?

If this looks good I'll implement it.

Comment thread models/sipnet/R/write.configs.SIPNET.R
Comment thread models/sipnet/inst/template.param_v2
Comment thread models/sipnet/R/write.configs.SIPNET.R
Comment thread models/sipnet/NEWS.md Outdated
@infotroph infotroph changed the title WIP: Sipnet v2 support Sipnet v2 support Mar 7, 2026
Comment thread CHANGELOG.md Outdated
Comment thread CHANGELOG.md Outdated
Comment thread models/sipnet/NEWS.md Outdated
Comment thread models/sipnet/NEWS.md Outdated
Comment thread models/sipnet/R/met2model.SIPNET.R Outdated
Comment thread models/sipnet/R/met2model.SIPNET.R Outdated
Comment thread models/sipnet/R/met2model.SIPNET.R Outdated
Comment thread models/sipnet/R/write.configs.SIPNET.R Outdated
Comment on lines +19 to +28
# determine which SIPNET major version we're configuring for.
# settings$model$revision may be a SIPNET version ("1", "2", "2.0")
# or a internal model identifier ("136", "unk", "git", NULL).
# Only "1" and "2" (and their dotted forms) are valid SIPNET versions;
# everything else defaults to v1 for backward compatibility
rev_str <- "v1"
if (!is.null(rev_raw) && nzchar(rev_raw)) {
if (grepl("^[0-9]+$", rev_raw)) {
rev_raw <- paste0(rev_raw, ".0")
}
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This feels like a lot of complication to me, especially since it lets us accept a bare integer but not "v<integer>"; IMO the latter is quite a bit more widely used.

I wonder if we should instead require a numeric version for anything but an enumerated set of legacy versions (maybe just 136 and git, dropping unk?)

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.

I think the right approach would be to simplify to accept "v1", "v2", "1", "2" and default everything else to v1

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.

may be

rev_raw <- settings$model$revision %||% "1"
# accept "v1", "v2", "1", "2", "1.0", "2.0"; legacy DB identifiers (136, git) default to v1
rev_str <- if (grepl("^v?2(\\.0)?$", rev_raw, ignore.case = TRUE)) "v2" else "v1"

make sense ?

Copy link
Copy Markdown
Member Author

@infotroph infotroph Mar 8, 2026

Choose a reason for hiding this comment

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

right approach would be to simplify to accept "v1", "v2", "1", "2"

If you're saying accept those but reject x.y.z numeric versions, I disagree. settings$model$revision is declaring the version of Sipnet that will actually run when you call settings$model$binary, and needs to be interpretable by any function that needs it and not just run.write.configs.

Say hypothetically we do a couple releases with restart support and then decide we want to change its API without breaking support for the previous ones; we'd want to be able to add conditions like if (settings$model$revision >= "2.0.3") {... into write.restart without breaking the write.configs v1/v2 logic.

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.

enumerated known legacy identifiers ("136", "git") and map them to v1 explicitly;
sipnet_version variable is also available in function scope for any future >= comparisons (e.g. >= "2.0.3" check for api changes)

3f36714

Comment thread models/sipnet/NEWS.md
@divine7022
Copy link
Copy Markdown
Member

https://github.com/PecanProject/pecan/actions/runs/22837731305/job/66245148269?pr=3813

is this an upstream issue with how the sipnet v1.3.0 binary interacts with the current gha runner ?

@infotroph
Copy link
Copy Markdown
Member Author

infotroph commented Mar 9, 2026

@divine7022 Took me a while to track this down (had to run the Github action locally), but: Read unexpected input item: GROWTH_RESP, because the GHA Bety returns revision "102319", which compares >= "2.0", so PEcAn.SIPNET uses the v2 parameter template.

Comment thread models/sipnet/R/write.configs.SIPNET.R Outdated
@dlebauer dlebauer added this pull request to the merge queue Mar 10, 2026
Merged via the queue into PecanProject:develop with commit b15916b Mar 10, 2026
19 of 25 checks passed
@infotroph infotroph deleted the sipnet-v2 branch March 28, 2026 08:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants