Skip to content
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
9f91be1
Add balance.c|h
Alomir Feb 3, 2026
7b182f7
Improve do-single-output help
Alomir Feb 3, 2026
0ec16f7
Carbon and nitrogen balance checks
Alomir Feb 3, 2026
2bfd757
Add event fluxes to track system inputs and outputs; add nppStorage p…
Alomir Feb 3, 2026
f6f89d8
Add external writeEvent function, refactor original
Alomir Feb 3, 2026
9aa91fb
Add event for leaf on/off; split woodC into two pools; add balance ch…
Alomir Feb 3, 2026
0c454bf
Updates for balance checks and leaf on/off events
Alomir Feb 3, 2026
ec9b469
Merge branch 'master' into add_carbon_balance_check
Alomir Feb 3, 2026
8ba00eb
Add ability to compare to base
Alomir Feb 3, 2026
b2e26c4
Remove tolerance from balance tracker
Alomir Feb 3, 2026
67028b1
Update col list for niwot
Alomir Feb 3, 2026
13faa6d
Avoid negative zero
Alomir Feb 3, 2026
9211161
Remove negative zeros
Alomir Feb 3, 2026
fee673b
Add test for balance check
Alomir Feb 5, 2026
5b5c64a
Merge branch 'master' into add_carbon_balance_check
Alomir Feb 5, 2026
17da4b6
PR feedback
Alomir Feb 5, 2026
bae820e
Turn off microbes, as it is now broken
Alomir Feb 5, 2026
83afb27
Test updates after balance check updates
Alomir Feb 5, 2026
1aacd4c
Split leaf-on carbon creation into own flux
Alomir Feb 5, 2026
14f6d4a
Fix leafOn flux
Alomir Feb 5, 2026
2a8abae
Updates for leafOn split
Alomir Feb 5, 2026
b38d604
Force skipping russell_4
Alomir Feb 5, 2026
75a44c3
Update for new event fluxes
Alomir Feb 5, 2026
649b35b
Initial plan
Copilot Feb 6, 2026
02f68a4
Fix typos and clarify plantWoodCStorageDelta comment
Copilot Feb 6, 2026
df0257d
Add documentation for carbon balance checks and update CHANGELOG
Copilot Feb 6, 2026
63c6504
Address code review feedback: clarify nppStorage comment and fix bala…
Copilot Feb 6, 2026
f48c49c
Documentation and feedback complete
Copilot Feb 6, 2026
a248978
Add CodeQL build artifacts to .gitignore
Copilot Feb 6, 2026
1a071ee
Update documentation for storage pool, remove Mass Balance section pe…
Copilot Feb 6, 2026
305769d
Remove test output files and fix model-structure.md per review feedback
Copilot Feb 6, 2026
f127f8b
Merge branch 'master' into copilot/sub-pr-248-again
Alomir Feb 6, 2026
c32eb91
Merge branch 'master' into copilot/sub-pr-248-again
Alomir Feb 9, 2026
5a61240
Removed code param reference
Alomir Feb 9, 2026
b7ad6cd
Update C_wood,storage explanation, reformat
Alomir Feb 9, 2026
77f016b
Tweak line-wraps
Alomir Feb 9, 2026
56e2806
Merge branch 'master' into copilot/sub-pr-248-again
Alomir Feb 10, 2026
c0f0ad7
Merge branch 'master' into copilot/sub-pr-248-again
Alomir Feb 13, 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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ transpose
.mkdocs.stamp
site/*
site_preview/*
_codeql_build_dir/
_codeql_detected_source_root

# documentation
docs/api
Expand Down
3 changes: 3 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ add_library(sipnetlib
src/sipnet/runmean.c
src/sipnet/sipnet.c
src/sipnet/state.c
src/sipnet/balance.c
)

add_library(tests
Expand All @@ -52,4 +53,6 @@ add_library(tests
tests/sipnet/test_events_types/testEventTillage.c
tests/sipnet/test_bugfixes/testEventFileOrderChecks.c
tests/sipnet/test_modeling/testNitrogenCycle.c
tests/sipnet/test_modeling/testDependencyFunctions.c
tests/sipnet/test_modeling/testBalance.c
)
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ COMMON_CFILES:=context.c logging.c modelParams.c util.c
COMMON_CFILES:=$(addprefix src/common/, $(COMMON_CFILES))
COMMON_OFILES=$(COMMON_CFILES:.c=.o)

SIPNET_CFILES:=sipnet.c cli.c events.c frontend.c outputItems.c runmean.c state.c
SIPNET_CFILES:=sipnet.c cli.c events.c frontend.c outputItems.c runmean.c state.c balance.c
SIPNET_CFILES:=$(addprefix src/sipnet/, $(SIPNET_CFILES))
SIPNET_OFILES=$(SIPNET_CFILES:.c=.o)
SIPNET_LIBS=-lsipnet_common
Expand Down
3 changes: 3 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ sections to include in release notes:
- Organic N handling for fertilization and soil dynamics (#199)
- Dynamic C:N for soil and litter (#214)
- Updates for soil and litter respiration (tillage and C:N effects) (#214)
- Carbon and nitrogen mass balance checks (#248)
- Event tracking for leaf-on and leaf-off phenological transitions (#248)
- Split wood carbon pool into `plantWoodC` and `plantWoodCStorageDelta` to track NPP storage lag (#248)

### Fixed

Expand Down
19 changes: 13 additions & 6 deletions docs/model-structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,25 +132,32 @@ Plant death is implemented as a harvest event with the fraction of biomass trans

As stated above, SIPNET uses a five-day averaged NPP when allocating gained carbon to plant growth. To implement this,
the current timestep's net primary production (adjusted GPP - autotrophic respiration) is added to the wood carbon pool
where it acts as an _implicit_ storage pool, and all allocations from the averaged NPP are deducted from that pool.
We can represent this storage of carbon conceptually as:
where it acts as a storage pool, and all allocations from the averaged NPP are deducted from that pool.

Starting in SIPNET v2.1, to support accurate mass balance tracking, this storage is explicitly tracked as a separate pool called $C_{\text{wood,storage}}$ (`plantWoodCStorageDelta` in code). We can represent this storage of carbon as:
Copy link
Member

Choose a reason for hiding this comment

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

Remove "accurate". It is implied. Don't add parameter name used in code here, that isn't done elsewhere in this file. The mapping between mathematical symbols and parameter names are in parameters.md. Does the document have sufficient description of the dynamics added in the PR that this PR will be merged into? If needed, make sure it is consistent with the document as a whole and the current state of the code.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 305769d:

  • Removed "accurate" qualifier
  • Removed code parameter name (plantWoodCStorageDelta)
  • Line now reads: "Starting in SIPNET v2.1, to support mass balance tracking, this storage is explicitly tracked as a separate pool called $C_{\text{wood,storage}}$."

The mathematical notation $C_{\text{wood,storage}}$ is mapped to the code name in parameters.md as requested.


\begin{equation}
NPP_\text{storage} = (GPP - R_a) - \overline{\text{NPP}}_\text{alloc}
C_{\text{wood,storage}} = (GPP - R_a) - \overline{\text{NPP}}_\text{alloc}
\end{equation}

where $\overline{NPP}_\text{alloc}$ is the sum of the carbon allocated to the biomass pools as growth. Note that we do not explicitly track this storage term.
where $\overline{NPP}_\text{alloc}$ is the sum of the carbon allocated to the biomass pools as growth. This storage term represents the lag between NPP input and allocation output due to the five-day averaging.

The total wood carbon is the sum of the structural wood carbon and the storage pool:

\begin{equation}
C_{\text{wood,total}} = C_{\text{wood}} + C_{\text{wood,storage}}
\end{equation}

Thus, changes to wood carbon over time are determined by:

\begin{equation}
\frac{dC_\text{wood}}{dt} = NPP_\text{storage} + \alpha_\text{wood} \cdot \overline{\text{NPP}} - F^C_\text{litter,wood}
\frac{dC_\text{wood}}{dt} = C_{\text{wood,storage}} + \alpha_\text{wood} \cdot \overline{\text{NPP}} - F^C_\text{litter,wood}
\label{eq:Braswell_A1}
\end{equation}

where $\alpha_\text{wood}\cdot\overline{\text{NPP}}$ represents the amount of carbon allocated to growth and $(F^C_\text{litter,wood})$ is the wood litter production.

This is equation (A1) from Braswell, et al. (2005), manipulated to use NPP_\text{storage}.
This is equation (A1) from Braswell, et al. (2005), modified to explicitly track the storage component.

### Leaf Carbon

Expand Down
1 change: 1 addition & 0 deletions docs/parameters.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ Run-time parameters can change from one run to the next, or when the model is st
| | Symbol | Parameter Name | Definition | Units | notes |
| --- | -------------------------- | --------------- | ------------------------------------------------------------------------ | ---------------------------------------------------- | ---------------------------------------------------------------------------------- |
| 1 | $C_{\text{wood},0}$ | plantWoodInit | Initial wood carbon | $\text{g C} \cdot \text{m}^{-2} \text{ ground area}$ | above-ground + roots |
| | $C_{\text{wood,storage}}$ | | Wood carbon storage pool (delta) | $\text{g C} \cdot \text{m}^{-2} \text{ ground area}$ | Tracks lag between NPP input and allocation output due to 5-day averaging; initialized to 0; can be negative. In code: `plantWoodCStorageDelta`. Total wood C = $C_{\text{wood}} + C_{\text{wood,storage}}$ |
| 2 | $LAI_0$ | laiInit | Initial leaf area | m^2 leaves \* m^-2 ground area | multiply by SLW to get initial plant leaf C: $C_{\text{leaf},0} = LAI_0 \cdot SLW$ |
| 3 | $C_{\text{litter},0}$ | litterInit | Initial litter carbon | $\text{g C} \cdot \text{m}^{-2} \text{ ground area}$ | |
| 4 | $C_{\text{soil},0}$ | soilInit | Initial soil carbon | $\text{g C} \cdot \text{m}^{-2} \text{ ground area}$ | |
Expand Down
124 changes: 124 additions & 0 deletions src/sipnet/balance.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
#include "balance.h"

#include <math.h>

#include "common/context.h"
#include "common/exitCodes.h"
#include "common/logging.h"
#include "state.h"

// Definition of global balance tracker struct
BalanceTracker balanceTracker;

void getMassTotals(double *carbon, double *nitrogen) {
*carbon = (envi.plantWoodC + envi.plantWoodCStorageDelta) + envi.plantLeafC +
envi.fineRootC + envi.coarseRootC + envi.soilC;
if (ctx.litterPool) {
*carbon += envi.litterC;
}

if (ctx.nitrogenCycle) {
// Note: this is the one place where we ust plantWoodC by itself; it's the
// reason plantNSCWoodCDelta was created, so that we can ignore it here.
*nitrogen =
envi.plantWoodC / params.woodCN + envi.plantLeafC / params.leafCN +
envi.fineRootC / params.fineRootCN + envi.coarseRootC / params.woodCN +
envi.soilOrgN + envi.litterN + envi.minN;
} else {
*nitrogen = 0.0;
}
}

void updateBalanceTrackerPreUpdate(void) {
// Set the pre-update pool totals
getMassTotals(&balanceTracker.preTotalC, &balanceTracker.preTotalN);
}

void updateBalanceTrackerPostUpdate(void) {
// Set the post-update pool totals
getMassTotals(&balanceTracker.postTotalC, &balanceTracker.postTotalN);

// Calculate the system inputs and outputs
// CARBON
balanceTracker.inputsC = fluxes.photosynthesis + // GPP
fluxes.eventInputC; // agro event additions
balanceTracker.outputsC = fluxes.rVeg + fluxes.rFineRoot +
fluxes.rCoarseRoot + // R_a
fluxes.rSoil + // R_h
fluxes.eventOutputC; // agro event removals
if (ctx.litterPool) {
balanceTracker.outputsC += fluxes.rLitter;
}
// Account for climate length
balanceTracker.inputsC *= climate->length;
balanceTracker.outputsC *= climate->length;

// NITROGEN
if (ctx.nitrogenCycle) {
balanceTracker.inputsN =
// TODO: fluxes.fixation +
fluxes.eventInputN;
balanceTracker.outputsN =
fluxes.nLeaching + fluxes.nVolatilization + fluxes.eventOutputN;

// Account for climate length
balanceTracker.inputsN *= climate->length;
balanceTracker.outputsN *= climate->length;
}
}

void initBalanceTracker(void) {
// Initialize all to zero
balanceTracker.preTotalC = 0.0;
balanceTracker.postTotalC = 0.0;
balanceTracker.inputsC = 0.0;
balanceTracker.outputsC = 0.0;
balanceTracker.preTotalN = 0.0;
balanceTracker.postTotalN = 0.0;
balanceTracker.inputsN = 0.0;
balanceTracker.outputsN = 0.0;
balanceTracker.deltaC = 0.0;
balanceTracker.deltaN = 0.0;
}

void checkBalance(void) {
// CARBON
// Pool delta
double poolCDelta = balanceTracker.postTotalC - balanceTracker.preTotalC;
// System delta
double systemCDelta = balanceTracker.inputsC - balanceTracker.outputsC;
balanceTracker.deltaC = poolCDelta - systemCDelta;

// NITROGEN
// Pool delta
double poolNDelta = balanceTracker.postTotalN - balanceTracker.preTotalN;
// System delta
double systemNDelta = balanceTracker.inputsN - balanceTracker.outputsN;
balanceTracker.deltaN = poolNDelta - systemNDelta;

// To avoid weird negative-zero issues...
if (fabs(balanceTracker.deltaC) < 1e-8) {
balanceTracker.deltaC = 0.0;
}
if (fabs(balanceTracker.deltaN) < 1e-8) {
balanceTracker.deltaN = 0.0;
}

int err = 0;
if (fabs(balanceTracker.deltaC) > 0.0) {
err = 1;
logInternalError(
"Carbon balance check failed (delta=%8.4f, Y: %d D: %d T: %4.2f)\n",
balanceTracker.deltaC, climate->year, climate->day, climate->time);
}
// RE-ENABLE WHEN N IS FIXED
// if (fabs(balanceTracker.deltaN) > 0.0) {
// err = 1;
// logInternalError("Nitrogen balance check failed (delta=%8.4f)\n",
// balanceTracker.deltaN);
//}
if (err) {
logInternalError("Exiting\n");
// exit(EXIT_CODE_INTERNAL_ERROR);
}
}
41 changes: 41 additions & 0 deletions src/sipnet/balance.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#ifndef BALANCE_H
#define BALANCE_H

typedef struct BalanceTrackerStruct {
// Mass balance checks:
// X_t - X_(t-1) = inputs - outputs + tolerance
// or, we check that
// (pool delta) + (system delta) < tolerance
// where
// pool delta = X_t - X_(t-1)
// system delta = outputs - inputs

// Carbon balance
double preTotalC;
double postTotalC;
double inputsC;
double outputsC;

// Nitrogen balance
double preTotalN;
double postTotalN;
double inputsN;
double outputsN;

// Checks
double deltaC;
double deltaN;
} BalanceTracker;

// Global var
extern BalanceTracker balanceTracker;

void updateBalanceTrackerPreUpdate(void);

void updateBalanceTrackerPostUpdate(void);

void initBalanceTracker(void);

void checkBalance(void);

#endif // BALANCE_H
4 changes: 3 additions & 1 deletion src/sipnet/cli.c
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ void usage(char *progName) {
printf("\n");
printf("Output flags: (prepend flag with 'no-' to force off, eg '--no-print-header')\n");
printf(" --do-main-output Print time series of all output variables to <file-name>.out (1)\n");
printf(" --do-single-outputs Print outputs one variable per file (e.g. <file-name>.NEE)\n");
printf(" --do-single-outputs Print selection* of outputs one variable per file (e.g. <file-name>.NEE)\n");
printf(" --dump-config Print final config to <file-name>.config (0)\n");
printf(" --print-header Whether to print header row in output files (1)\n");
printf(" --quiet Suppress info and warning message (0)\n");
Expand All @@ -98,6 +98,8 @@ void usage(char *progName) {
printf(" -h, --help Print this message and exit\n");
printf(" -v, --version Print version information and exit\n");
printf("\n");
printf("*do-single-outputs option outputs are: NEE, NEE_cum, GPP, GPP_cum\n");
printf("\n");
printf("Configuration options are read from <input_file>. Other options specified on the command\n");
printf("line override settings from that file.\n");
printf("\n");
Expand Down
Loading