From cd64149d79a944c39946836442dc6b71bfd79e9a Mon Sep 17 00:00:00 2001 From: David LeBauer Date: Tue, 29 Sep 2026 13:57:46 -0700 Subject: [PATCH] Fix zero-carbon nitrogen transfer --- docs/CHANGELOG.md | 3 ++ src/sipnet/nitrogen.c | 18 +++++---- .../sipnet/test_modeling/testNitrogenCycle.c | 40 +++++++++++++++++++ 3 files changed, 53 insertions(+), 8 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index cdc5b4a6..9a83762e 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -15,6 +15,9 @@ sections to include in release notes: ### Fixed +- Prevent zero carbon transfers from producing non-finite nitrogen fluxes when + litter or soil carbon pools are empty. + ### Changed ### Removed diff --git a/src/sipnet/nitrogen.c b/src/sipnet/nitrogen.c index 739eb290..21fea522 100644 --- a/src/sipnet/nitrogen.c +++ b/src/sipnet/nitrogen.c @@ -49,14 +49,16 @@ static void calcNPoolFluxes(void) { // for both litter and soil, mineralization is calculated as heterotrophic // respiration divided by the C:N ratio of that pool. - double litterMin = fluxes.rLitter / litterCN; - double soilMin = fluxes.rSoil / soilCN; + // A zero carbon transfer carries no nitrogen, including from an empty pool. + double litterMin = fluxes.rLitter == 0.0 ? 0.0 : fluxes.rLitter / litterCN; + double soilMin = fluxes.rSoil == 0.0 ? 0.0 : fluxes.rSoil / soilCN; // Adding soil carbon saturation functionality so organic N fluxes to soil // and litter are proportional to respective carbon fluxes dependent on // soil carbon saturation - double soilNInputs = fluxes.litterToSoil / litterCN + - fluxes.fineRootLoss / params.fineRootCN + + double litterNToSoil = + fluxes.litterToSoil == 0.0 ? 0.0 : fluxes.litterToSoil / litterCN; + double soilNInputs = litterNToSoil + fluxes.fineRootLoss / params.fineRootCN + fluxes.coarseRootLoss / params.woodCN; // saturationFraction capped between zero and one double saturationFraction = @@ -67,10 +69,10 @@ static void calcNPoolFluxes(void) { // The litter org N flux is determined by the carbon fluxes from wood and leaf // litter (modified by leaf N resorption), and N loss due to mineralization. // N added via fertilization is handled elsewhere. - fluxes.nOrgLitter = - fluxes.leafLitter / params.leafCN - fluxes.leafOffNResorption + - fluxes.woodLitter / params.woodCN - litterMin - - fluxes.litterToSoil / litterCN + (soilNInputs * saturationFraction); + fluxes.nOrgLitter = fluxes.leafLitter / params.leafCN - + fluxes.leafOffNResorption + + fluxes.woodLitter / params.woodCN - litterMin - + litterNToSoil + (soilNInputs * saturationFraction); // soil // The soil org N flux is determined by the carbon flux from the litter pool, diff --git a/tests/sipnet/test_modeling/testNitrogenCycle.c b/tests/sipnet/test_modeling/testNitrogenCycle.c index 95e419c0..d5c96cf5 100644 --- a/tests/sipnet/test_modeling/testNitrogenCycle.c +++ b/tests/sipnet/test_modeling/testNitrogenCycle.c @@ -502,6 +502,45 @@ int testOrganicN(void) { return status; } +int testZeroCarbonNitrogenTransfer(void) { + int status = 0; + logTest("Running testZeroCarbonNitrogenTransfer\n"); + + for (int organicNPresent = 0; organicNPresent <= 1; organicNPresent++) { + resetState(); + envi.litterC = 0.0; + envi.soilC = 0.0; + envi.minN = 2.0; + envi.litterN = organicNPresent ? 0.0211 : 0.0; + envi.soilOrgN = organicNPresent ? 127.7255 : 0.0; + + double initialTotalN = envi.minN + envi.litterN + envi.soilOrgN; + double initialLitterN = envi.litterN; + double initialSoilOrgN = envi.soilOrgN; + + calcNPoolFluxes(); + updateNitrogenPools(); + + if (!isfinite(envi.minN) || !isfinite(envi.litterN) || + !isfinite(envi.soilOrgN)) { + logTest("Zero-carbon pools produced non-finite nitrogen stocks\n"); + status = 1; + } + status |= checkFlux(fluxes.nMin, 0.0, "Zero-carbon mineralization"); + status |= checkFlux(fluxes.nOrgLitter, 0.0, "Zero-carbon organic litter N"); + status |= checkFlux(fluxes.nOrgSoil, 0.0, "Zero-carbon organic soil N"); + status |= checkFlux(envi.minN, 2.0, "Zero-carbon mineral N pool"); + status |= + checkFlux(envi.litterN, initialLitterN, "Zero-carbon litter N pool"); + status |= checkFlux(envi.soilOrgN, initialSoilOrgN, + "Zero-carbon soil organic N pool"); + status |= checkFlux(envi.minN + envi.litterN + envi.soilOrgN, initialTotalN, + "Zero-carbon total N conservation"); + } + + return status; +} + ///// // Organic N with leafOffNResorption int testOrganicNWithResorption(void) { @@ -841,6 +880,7 @@ int run(void) { status |= testFertilization(); status |= testNLeaching(); status |= testOrganicN(); + status |= testZeroCarbonNitrogenTransfer(); status |= testLeafOnNFromC(); status |= testNFixation(); status |= testNLimitation();