Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ sections to include in release notes:
### Changed

- Renamed the CLI option `--file-name` to `--file-prefix` for clarity while keeping `--file-name` as a backward-compatible alias (#320)
- Renamed `plantWoodCStorageDelta` to `plantWoodCAccountingDelta` and output column `nppStorage` to `plantWoodCAccountingDelta` to reflect that the field is an accounting term for carbon not coupled to nitrogen, not a storage pool (#339).

### Removed

Expand Down
2 changes: 2 additions & 0 deletions docs/developer-guide/restart-checkpoint.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ Event files must be segmented to the same time boundaries as climate segments.

## When Saved State Changes

If you rename a serialized restart key without changing struct layout (for example `envi.plantWoodCStorageDelta` → `envi.plantWoodCAccountingDelta`), update the key in `src/sipnet/restart.c` and restart tests; checkpoints written by older builds with the previous key name will fail to load.

If you add saved state or change an existing saved payload:

1. Update the serialized payload type and restart read/write logic in `src/sipnet/restart.c`.
Expand Down
31 changes: 16 additions & 15 deletions docs/model-structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,26 +163,27 @@ litter, $f_{\text{harvest,transfer,}i}$ set to 1.
### Wood Carbon

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 a storage pool, and all allocations from the averaged NPP are deducted from that pool.
the current timestep's net primary production (adjusted GPP - autotrophic respiration) is accumulated in an accounting
term, and all allocations from the averaged NPP are deducted from that term.

Starting in SIPNET v2.1, to support mass balance tracking, this storage is explicitly tracked as a separate pool called
$C_{\text{wood,storage}}$, which is initialized to zero. We can represent this storage of carbon as:
Starting in SIPNET v2.1, to support mass balance tracking, this term is explicitly tracked as
$C_{\text{wood,accounting}}$ (`plantWoodCAccountingDelta`), which is initialized to zero. We can represent this
accounting delta as:

\begin{equation}
\frac{dC_{\text{wood,storage}}}{dt} = (GPP - R_a) - \overline{\text{NPP}}_\text{alloc}
\label{eq:wood_c_storage}
\frac{dC_{\text{wood,accounting}}}{dt} = (GPP - R_a) - \overline{\text{NPP}}_\text{alloc}
\label{eq:wood_c_accounting}
\end{equation}

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. Tracking the storage term
explicitly enables checking nitrogen mass balance, as the storage changes do not involve nitrogen changes, whereas
growth allocation does.
where $\overline{NPP}_\text{alloc}$ is the sum of the carbon allocated to the biomass pools as growth. This accounting
term represents the lag between NPP input and allocation output due to the five-day averaging. Tracking it explicitly
enables checking nitrogen mass balance, as these carbon changes do not involve nitrogen changes, whereas growth
allocation does.

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

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

Thus, changes to (non-storage) wood carbon over time are determined by:
Expand Down Expand Up @@ -695,8 +696,8 @@ Nitrogen limitation occurs when plant nitrogen demand exceeds the supply of plan
demand is diagnosed from potential biomass growth derived from five-day averaged NPP.

If plant nitrogen demand exceeds plant-available nitrogen, allocation of carbon to new growth is reduced to the level
that available nitrogen can support. Carbon not allocated to growth remains in the wood storage pool
(\eqref{eq:wood_c_storage}). Thus, nitrogen limitation does not directly affect carbon uptake; it reduces future
that available nitrogen can support. Carbon not allocated to growth remains in the wood carbon accounting term
(\eqref{eq:wood_c_accounting}). Thus, nitrogen limitation does not directly affect carbon uptake; it reduces future
photosynthesis by constraining increases in photosynthetically active leaf area \eqref{eq:lai_calculation}.

Nitrogen limitation is applied during the flux calculation stage of the model update sequence. N limitation is
Expand All @@ -709,7 +710,7 @@ implemented as follows:
- Reduce biomass growth accordingly by scaling carbon allocation to plant biomass pools.
- Calculate the amount by which plant N demand exceeds available supply [^*].
- Calculate the fraction by which biomass growth must be reduced so that N demand equals supply.
- Reduce biomass growth accordingly by scaling carbon allocation to plant biomass pools. Unallocated carbon remains in the wood storage pool \eqref{eq:wood_c_storage}.
- Reduce biomass growth accordingly by scaling carbon allocation to plant biomass pools. Unallocated carbon remains in the wood carbon accounting term \eqref{eq:wood_c_accounting}.
- Calculate nitrogen uptake as the amount of N required to support the realized plant growth, based on fixed stoichiometry.

[^*]: Nitrogen limitation is evaluated after accounting for biological nitrogen fixation and before mineral nitrogen
Expand Down
2 changes: 1 addition & 1 deletion docs/parameters.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ Run-time parameters can change from one run to the next, or when the model is st
| Symbol | Parameter Name | Definition | Units | Notes |
|-----------------------------------|----------------|--------------------------------------------------------------------------|---------------------------------------------------------------------|------------------------------------------------------------------------------------------|
| $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), initialized internally to 0 | $\text{g C} \cdot \text{m}^{-2} \text{ ground area}$ | Not a runtime param; $C_{\text{wood,total}} = C_{\text{wood}} + C_{\text{wood,storage}}$ |
| $C_{\text{wood,accounting}}$ | plantWoodCAccountingDelta | Wood carbon accounting delta (N-lag term), initialized internally to 0 | $\text{g C} \cdot \text{m}^{-2} \text{ ground area}$ | Not a runtime param; $C_{\text{wood,total}} = C_{\text{wood}} + C_{\text{wood,accounting}}$ |
| $LAI_0$ | laiInit | Initial leaf area | $\text{m}^2 \text{ leaves} \cdot \text{m}^{-2} \text{ ground area}$ | Multiply by SLW to get initial plant leaf C: $C_{\text{leaf},0} = LAI_0 \cdot SLW$ |
| $C_{\text{litter},0}$ | litterInit | Initial litter carbon | $\text{g C} \cdot \text{m}^{-2} \text{ ground area}$ | |
| $C_{\text{soil},0}$ | soilInit | Initial soil carbon | $\text{g C} \cdot \text{m}^{-2} \text{ ground area}$ | |
Expand Down
3 changes: 3 additions & 0 deletions docs/user-guide/model-outputs.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ The `sipnet.out` file contains a time series of state variables and fluxes from
| | | nFixation | Nitrogen demand met by fixation | g N m$^{-2}$ |
| | | nUptake | Nitrogen demand met by uptake from soil | g N m$^{-2}$ |
| | | ch4 | Methane production | g C m$^{-2}$ |
| | $C_{\text{wood,accounting}}$ | plantWoodCAccountingDelta | Wood carbon accounting delta (N-lag term; not N-coupled structural wood) | g C m$^{-2}$ |

Note: this column was previously named `nppStorage`.

[^1]: Mean soilWetnessFrac (ratio of soil water / water holding capacity) calculated as average between previous and current time step. Reported for diagnostics only.
Internal moisture dependency functions use instantaneous $W_{soil}/W_{WHC}$ (not this average), and clip that ratio to [0,1] where those dependency functions are defined.
Expand Down
7 changes: 4 additions & 3 deletions src/sipnet/balance.c
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,16 @@
BalanceTracker balanceTracker;

void getMassTotals(double *carbon, double *nitrogen) {
*carbon = (envi.plantWoodC + envi.plantWoodCStorageDelta) + envi.plantLeafC +
envi.fineRootC + envi.coarseRootC + envi.soilC;
*carbon = (envi.plantWoodC + envi.plantWoodCAccountingDelta) +
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 use plantWoodC by itself; it's the
// reason plantWoodCStorageDelta was created, so that we can ignore it here.
// reason plantWoodCAccountingDelta 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 +
Expand Down
2 changes: 1 addition & 1 deletion src/sipnet/events.c
Original file line number Diff line number Diff line change
Expand Up @@ -531,7 +531,7 @@ void processEvents(void) {
const double fracRB = harvParams->fractionRemovedBelow;
const double fracTB = harvParams->fractionTransferredBelow;

const double woodC = envi.plantWoodC + envi.plantWoodCStorageDelta;
const double woodC = envi.plantWoodC + envi.plantWoodCAccountingDelta;
// Litter increase
double litterAdd = fracTA * (envi.plantLeafC + woodC);
double soilAdd = fracTB * (envi.fineRootC + envi.coarseRootC);
Expand Down
2 changes: 1 addition & 1 deletion src/sipnet/restart.c
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ void initResetState(RestartState *state, MeanTracker *npp) {
state->enviPF[9] = (StateField){"envi.soilOrgN", FT_DOUBLE, &envi.soilOrgN, 0};
state->enviPF[10] = (StateField){"envi.litterN", FT_DOUBLE, &envi.litterN, 0};
state->enviPF[11] = (StateField){"envi.plantStorageN", FT_DOUBLE, &envi.plantStorageN, 0};
state->enviPF[12] = (StateField){"envi.plantWoodCStorageDelta", FT_DOUBLE, &envi.plantWoodCStorageDelta, 0};
state->enviPF[12] = (StateField){"envi.plantWoodCAccountingDelta", FT_DOUBLE, &envi.plantWoodCAccountingDelta, 0};
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 don't think we want to support the legacy key

state->enviPF[13] = (StateField){"envi.invalid", FT_INVALID, NULL, FIELD_INVALID};

state->trackersPF[0] = (StateField){"trackers.gpp", FT_DOUBLE, &trackers.gpp, 0};
Expand Down
18 changes: 9 additions & 9 deletions src/sipnet/sipnet.c
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,7 @@ void outputHeader(FILE *out) {
fprintf(out,
"fluxestranspiration minN soilOrgN litterN "
"plantStorageN n2o nLeaching nFixation nUptake ch4 "
"nppStorage\n");
"plantWoodCAccountingDelta\n");
}

/*!
Expand All @@ -450,7 +450,7 @@ void outputHeader(FILE *out) {
void outputState(FILE *out, int year, int day, double time) {

fprintf(out, "%4d %3d %5.2f %10.2f %10.2f %12.2f ", year, day, time,
(envi.plantWoodC + envi.plantWoodCStorageDelta), envi.plantLeafC,
(envi.plantWoodC + envi.plantWoodCAccountingDelta), envi.plantLeafC,
trackers.woodCreation);
fprintf(out, "%8.2f ", envi.soilC);
fprintf(out, "%11.2f %9.2f ", envi.coarseRootC, envi.fineRootC);
Expand All @@ -467,7 +467,7 @@ void outputState(FILE *out, int year, int day, double time) {
fprintf(out, "%9.6f %9.4f %10.4f %8.4f %8.4f", trackers.n2o,
trackers.nLeaching, trackers.nFixation, trackers.nUptake,
trackers.methane);
fprintf(out, "%12.4f\n", envi.plantWoodCStorageDelta);
fprintf(out, "%27.4f\n", envi.plantWoodCAccountingDelta);
}

// de-allocate space used for climate linked list
Expand Down Expand Up @@ -1072,7 +1072,7 @@ void vegResp(double *folResp, double *woodResp, double baseFolResp) {

// :: from [1], eq (A19)
*woodResp = params.baseVegResp *
(envi.plantWoodC + envi.plantWoodCStorageDelta) *
(envi.plantWoodC + envi.plantWoodCAccountingDelta) *
pow(params.vegRespQ10, climate->tair / 10.0);
}

Expand Down Expand Up @@ -1100,7 +1100,7 @@ void vegResp2(double *folResp, double *woodResp, double *growthResp,
// by a given fraction in winter
}
*woodResp = params.baseVegResp *
(envi.plantWoodC + envi.plantWoodCStorageDelta) *
(envi.plantWoodC + envi.plantWoodCAccountingDelta) *
pow(params.vegRespQ10, climate->tair / 10.0);

// Rg is a fraction of the recent mean NPP
Expand Down Expand Up @@ -1203,8 +1203,8 @@ void calcRootAndWoodFluxes(void) {

// Wood litter, in g C * m^-2 ground area * day^-1
// turnover rate is fraction lost per day
fluxes.woodLitter =
(envi.plantWoodC + envi.plantWoodCStorageDelta) * params.woodTurnoverRate;
fluxes.woodLitter = (envi.plantWoodC + envi.plantWoodCAccountingDelta) *
params.woodTurnoverRate;

// :: from [3], root model description
calcRootResp(&fluxes.rCoarseRoot, params.coarseRootQ10,
Expand Down Expand Up @@ -1608,7 +1608,7 @@ void updateMainPools(void) {
double r_a = fluxes.rVeg + fluxes.rFineRoot + fluxes.rCoarseRoot;
double nppAllocations = fluxes.leafCreation + fluxes.woodCreation +
fluxes.fineRootCreation + fluxes.coarseRootCreation;
envi.plantWoodCStorageDelta +=
envi.plantWoodCAccountingDelta +=
((fluxes.photosynthesis - r_a) - nppAllocations) * climate->length;
envi.plantWoodC += (fluxes.woodCreation - fluxes.woodLitter -
fluxes.leafOnCreationFromWood) *
Expand Down Expand Up @@ -1792,7 +1792,7 @@ void setupModel(void) {

envi.plantWoodC =
(1 - params.coarseRootFrac - params.fineRootFrac) * params.plantWoodInit;
envi.plantWoodCStorageDelta = 0.0;
envi.plantWoodCAccountingDelta = 0.0;
envi.plantLeafC = params.laiInit * params.leafCSpWt;

if (ctx.litterPool) {
Expand Down
11 changes: 6 additions & 5 deletions src/sipnet/state.h
Original file line number Diff line number Diff line change
Expand Up @@ -447,11 +447,12 @@ typedef struct Environment {
// Carbon gains from photosynthesis and losses to respiration don't affect the
// total nitrogen in the system. Since SIPNET uses a five-day time-averaged
// NPP value for allocating plant growth, there is a lag from input to output
// here that must be tracked. So, we split the wood pool into plantWoodC
// (above) and a new pool to track non-nitrogen-affecting changes over time.
// As this is a delta, it can be negative. Note that the actual "wood carbon"
// is the sum of these two pools.
double plantWoodCStorageDelta;
// here that must be tracked. plantWoodC (above) holds N-coupled structural
// wood; plantWoodCAccountingDelta tracks carbon changes with no associated
// nitrogen (an accounting term, not a storage pool). As this is a delta, it
// can be negative. Total wood carbon is plantWoodC +
// plantWoodCAccountingDelta.
double plantWoodCAccountingDelta;
} Envi;

// Global var
Expand Down
2 changes: 1 addition & 1 deletion tests/sipnet/test_events_types/testEventHarvest.c
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ void initEnv(void) {
}

// not used here, but accessed
envi.plantWoodCStorageDelta = 0.0;
envi.plantWoodCAccountingDelta = 0.0;
envi.soilWater = 10.0;
envi.minN = 10.0;
}
Expand Down
4 changes: 2 additions & 2 deletions tests/sipnet/test_restart_infrastructure/mock_state.c
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ int main(void) {

// Missed envi update
copyFile(STATE_FILE, BAD_STATE);
replaceFirstOccurrence(BAD_STATE, "double plantWoodCStorageDelta;",
"double plantWoodCStorageDelta;double dummyPool;");
replaceFirstOccurrence(BAD_STATE, "double plantWoodCAccountingDelta;",
"double plantWoodCAccountingDelta;double dummyPool;");

// Missed context update
copyFile(CONTEXT_FILE, BAD_CTX);
Expand Down
42 changes: 42 additions & 0 deletions tests/sipnet/test_restart_infrastructure/testRestartMVP.c
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include <stdlib.h>
#include <string.h>

#include "common/exitCodes.h"
#include "common/logging.h"
#include "utils/tUtils.h"

Expand Down Expand Up @@ -168,6 +169,46 @@ static int hasManagedEventOnDay(const char *eventFile, int year, int day) {
return found;
}

static int testObsoletePlantWoodCStorageDeltaKeyFails(void) {
int status = 0;
int stepStatus = 0;
int rc;

logTest("Starting testObsoletePlantWoodCStorageDeltaKeyFails\n");

runShell("rm -f run.out events.out run.restart *.log");

stepStatus = prepRunFiles("restart_segment1.clim", "events_segment1.in");
if (stepStatus) {
logTest(
"Failed to prepare files for obsolete restart key test segment 1\n");
return stepStatus;
}

status |= (runModel("restart_seg1.in", "obsolete_key_seg1.log") != 0);
status |= !fileContains(CHECKPOINT_FILE, "envi.plantWoodCAccountingDelta ");
status |=
replaceFirstOccurrence(CHECKPOINT_FILE, "envi.plantWoodCAccountingDelta ",
"envi.plantWoodCStorageDelta ");

stepStatus = prepRunFiles("restart_segment2.clim", "events_segment2.in");
if (stepStatus) {
logTest(
"Failed to prepare files for obsolete restart key test segment 2\n");
return status | stepStatus;
}

rc = runModel("restart_seg2.in", "obsolete_key_seg2.log");
status |= (rc != EXIT_CODE_BAD_RESTART_PARAMETER);
status |= !fileContains("obsolete_key_seg2.log", "unknown key");

if (status) {
logTest("testObsoletePlantWoodCStorageDeltaKeyFails failed (rc=%d)\n", rc);
}

return status;
}

static int testDefaultEventsFileUsedWhenUnset(void) {
int status = 0;
int stepStatus = 0;
Expand Down Expand Up @@ -757,6 +798,7 @@ int run(void) {
status |= testEventsPrefixCliOverrideUsed();
status |= testConfigDumpIncludesRestartAndEventsKeys();
status |= testSegmentedEquivalence();
status |= testObsoletePlantWoodCStorageDeltaKeyFails();
status |= testStrictClimateMismatchFails();
status |= testCheckpointFarFromMidnightWarnsAndWrites();
status |= testRestartNotNearMidnightWarns();
Expand Down
Loading
Loading