Skip to content
Draft
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
4 changes: 4 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ sections to include in release notes:
- Soil mineral pool (#170)
- Nitrogen effects of fertilization (#173)
- `logAppend` logging function (#173)
- Plant nitrogen pools with C:N ratios for leaf, wood, fine root, and coarse root
- Nitrogen demand calculation for plant growth (dN/dt = dC/dt / CN)
- Nitrogen limitation of NPP when plant N demand exceeds mineral N supply
- CN parameters: `leafCN`, `woodCN`, `fineRootCN`, `coarseRootCN`

### Fixed

Expand Down
4 changes: 4 additions & 0 deletions docs/parameters.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,10 @@ Run-time parameters can change from one run to the next, or when the model is st
| new | $N_{\text{min},0}$ | mineralNInit | Initial soil mineral nitrogen pool | g N m$^{-2}$ | Initializes $N_\text{min}$ |
| new | $K_\text{vol}$ | nVolatilizationFrac | Fraction of $N_\text{min}$ volatilized per day (modulated by temperature and moisture) | day$^{-1}$ | Eq. (17) |
| new | $f^N_{\text{leach}}$ | nLeachingFrac | Leaching coefficient applied to $N_\text{min}$ scaled by drainage | day$^{-1}$ | Eq. (18) |
| new | $CN_{\text{leaf}}$ | leafCN | Carbon to nitrogen ratio for leaves | g C / g N | Eq. (12) |
| new | $CN_{\text{wood}}$ | woodCN | Carbon to nitrogen ratio for wood | g C / g N | Eq. (12) |
| new | $CN_{\text{froot}}$ | fineRootCN | Carbon to nitrogen ratio for fine roots | g C / g N | Eq. (12) |
| new | $CN_{\text{croot}}$ | coarseRootCN | Carbon to nitrogen ratio for coarse roots | g C / g N | Eq. (12) |
| new | $f_{\text{fix,max}}$ | nFixFracMax | Maximum fraction of plant N demand that can be met by biological N fixation under low soil N | fraction | Eq. (19) |
| new | $K_N$ | nFixHalfSatMinN | Mineral N level at which fixation suppression factor $D_{N_\text{min}}$ equals 0.5 | g N m$^{-2}$ | Eq. (19a) |

Expand Down
Empty file added events.out
Empty file.
183 changes: 182 additions & 1 deletion src/sipnet/sipnet.c
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,10 @@ void readParamData(ModelParams **modelParamsPtr, const char *paramFile) {
initializeOneModelParam(modelParams, "litterOrgNInit", &(params.litterOrgNInit), ctx.nitrogenCycle);
initializeOneModelParam(modelParams, "nVolatilizationFrac", &(params.nVolatilizationFrac), ctx.nitrogenCycle);
initializeOneModelParam(modelParams, "nLeachingFrac", &(params.nLeachingFrac), ctx.nitrogenCycle);
initializeOneModelParam(modelParams, "leafCN", &(params.leafCN), ctx.nitrogenCycle);
initializeOneModelParam(modelParams, "woodCN", &(params.woodCN), ctx.nitrogenCycle);
initializeOneModelParam(modelParams, "fineRootCN", &(params.fineRootCN), ctx.nitrogenCycle);
initializeOneModelParam(modelParams, "coarseRootCN", &(params.coarseRootCN), ctx.nitrogenCycle);

// NOLINTEND
// clang-format on
Expand Down Expand Up @@ -1243,6 +1247,58 @@ void calcNLeachingFlux() {
fluxes.nLeaching = envi.minN * phi * params.nLeachingFrac;
}

/*!
* Calculate plant N demand and uptake
*
* Calculates N demand for each plant pool based on C flux and CN ratio (eq 12).
* Total demand is summed (eq 20) and compared to available mineral N.
* N uptake is the minimum of demand and available N supply.
*/
void calcPlantNDemandAndUptake() {
double nSupply;

// Calculate N demand for each pool: dN/dt = dC/dt / CN
// Positive fluxes only (growth) require N uptake

// Leaf N demand from leaf creation
if (fluxes.leafCreation > 0 && params.leafCN > 0) {
fluxes.leafNDemand = fluxes.leafCreation / params.leafCN;
} else {
fluxes.leafNDemand = 0;
}

// Wood N demand from wood creation
if (fluxes.woodCreation > 0 && params.woodCN > 0) {
fluxes.woodNDemand = fluxes.woodCreation / params.woodCN;
} else {
fluxes.woodNDemand = 0;
}

// Fine root N demand from fine root creation
if (fluxes.fineRootCreation > 0 && params.fineRootCN > 0) {
fluxes.fineRootNDemand = fluxes.fineRootCreation / params.fineRootCN;
} else {
fluxes.fineRootNDemand = 0;
}

// Coarse root N demand from coarse root creation
if (fluxes.coarseRootCreation > 0 && params.coarseRootCN > 0) {
fluxes.coarseRootNDemand = fluxes.coarseRootCreation / params.coarseRootCN;
} else {
fluxes.coarseRootNDemand = 0;
}

// Total plant N demand (eq 20: F^N_demand = sum dN_i/dt)
fluxes.plantNDemand = fluxes.leafNDemand + fluxes.woodNDemand +
fluxes.fineRootNDemand + fluxes.coarseRootNDemand;

// N supply is available mineral N per day
nSupply = envi.minN / climate->length;

// N uptake is minimum of demand and supply (F^N_uptake = min(N_min/dt, F^N_demand))
fluxes.plantNUptake = fmin(fluxes.plantNDemand, nSupply);
}

/*!
* Calculate flux terms for sipnet as part of main model flow
*
Expand Down Expand Up @@ -1308,10 +1364,54 @@ void calculateFluxes(void) {

// Nitrogen cycle
if (ctx.nitrogenCycle) {
// Initialize plant N flux variables
fluxes.plantNDemand = 0;
fluxes.plantNUptake = 0;
fluxes.leafNDemand = 0;
fluxes.woodNDemand = 0;
fluxes.fineRootNDemand = 0;
fluxes.coarseRootNDemand = 0;

calcNVolatilizationFlux();
// Leaching depends on drainage flux so makes sure calcNLeachingFlux
// occurs after calcSoilWaterFluxes
calcNLeachingFlux();

// Calculate plant N demand and uptake
calcPlantNDemandAndUptake();

// N limitation: if demand exceeds supply, limit NPP by setting GPP = R_A
// I_N = boolean(F^N_demand > N_min/dt)
// if(I_N): NPP = 0; this is done by setting GPP = R_A
// Note: nSupply was already calculated in calcPlantNDemandAndUptake
if (fluxes.plantNDemand > fluxes.plantNUptake) {
// N is limiting, set photosynthesis to equal autotrophic respiration
// This makes NPP = GPP - R_A = 0
fluxes.photosynthesis = fluxes.rVeg;

// Recalculate plant N demand and uptake with zero NPP
// This means no growth, so no N demand
fluxes.leafNDemand = 0;
fluxes.woodNDemand = 0;
fluxes.fineRootNDemand = 0;
fluxes.coarseRootNDemand = 0;
fluxes.plantNDemand = 0;
fluxes.plantNUptake = 0;

// Also need to zero out the growth fluxes since NPP = 0
fluxes.leafCreation = 0;
fluxes.woodCreation = 0;
fluxes.fineRootCreation = 0;
fluxes.coarseRootCreation = 0;
}
} else {
// Initialize to 0 when nitrogen cycle is off
fluxes.plantNDemand = 0;
fluxes.plantNUptake = 0;
fluxes.leafNDemand = 0;
fluxes.woodNDemand = 0;
fluxes.fineRootNDemand = 0;
fluxes.coarseRootNDemand = 0;
}
}

Expand Down Expand Up @@ -1396,6 +1496,10 @@ void ensureNonNegativeStocks(void) {
ensureNonNegative(&(envi.minN), 0);
ensureNonNegative(&(envi.soilOrgN), 0);
ensureNonNegative(&(envi.litterOrgN), 0);
ensureNonNegative(&(envi.plantLeafN), 0);
ensureNonNegative(&(envi.plantWoodN), 0);
ensureNonNegative(&(envi.fineRootN), 0);
ensureNonNegative(&(envi.coarseRootN), 0);
}

// update trackers at each time step
Expand Down Expand Up @@ -1605,7 +1709,54 @@ void updatePoolsForSoil(void) {
climate->length;

// Nitrogen Cycle
envi.minN -= (fluxes.nVolatilization + fluxes.nLeaching) * climate->length;
if (ctx.nitrogenCycle) {
// Update mineral N: subtract volatilization, leaching, and plant uptake
envi.minN -= (fluxes.nVolatilization + fluxes.nLeaching + fluxes.plantNUptake) *
climate->length;

// Update plant N pools based on growth and litter
// Calculate the fraction of N that each pool gets based on their demand
double k = 0.0; // allocation factor

if (fluxes.plantNDemand > 0) {
// k = (N uptake) / (N demand)
k = fluxes.plantNUptake / fluxes.plantNDemand;
}

// Update each plant N pool: dN/dt = (k * demand) - litter
// Leaf N
double leafNGrowth = k * fluxes.leafNDemand;
double leafNLitter = 0;
if (params.leafCN > 0) {
leafNLitter = fluxes.leafLitter / params.leafCN;
}
envi.plantLeafN += (leafNGrowth - leafNLitter) * climate->length;

// Wood N
double woodNGrowth = k * fluxes.woodNDemand;
double woodNLitter = 0;
if (params.woodCN > 0) {
woodNLitter = fluxes.woodLitter / params.woodCN;
}
envi.plantWoodN += (woodNGrowth - woodNLitter) * climate->length;

// Fine root N
double fineRootNGrowth = k * fluxes.fineRootNDemand;
double fineRootNLoss = 0;
if (params.fineRootCN > 0) {
fineRootNLoss = fluxes.fineRootLoss / params.fineRootCN;
}
envi.fineRootN += (fineRootNGrowth - fineRootNLoss) * climate->length;

// Coarse root N
double coarseRootNGrowth = k * fluxes.coarseRootNDemand;
double coarseRootNLoss = 0;
if (params.coarseRootCN > 0) {
coarseRootNLoss = fluxes.coarseRootLoss / params.coarseRootCN;
}
envi.coarseRootN += (coarseRootNGrowth - coarseRootNLoss) * climate->length;
}

// envi.soilOrgN += ... TBD
// envi.litterOrgN += ... TBD
}
Expand Down Expand Up @@ -1768,10 +1919,40 @@ void setupModel(void) {
envi.minN = params.minNInit;
envi.soilOrgN = params.soilOrgNInit;
envi.litterOrgN = params.litterOrgNInit;

// Initialize plant N pools based on C pools and CN ratios
// Only initialize if CN ratios are positive
if (params.leafCN > 0) {
envi.plantLeafN = envi.plantLeafC / params.leafCN;
} else {
envi.plantLeafN = 0.0;
}

if (params.woodCN > 0) {
envi.plantWoodN = envi.plantWoodC / params.woodCN;
} else {
envi.plantWoodN = 0.0;
}

if (params.fineRootCN > 0) {
envi.fineRootN = envi.fineRootC / params.fineRootCN;
} else {
envi.fineRootN = 0.0;
}

if (params.coarseRootCN > 0) {
envi.coarseRootN = envi.coarseRootC / params.coarseRootCN;
} else {
envi.coarseRootN = 0.0;
}
} else {
envi.minN = 0.0;
envi.soilOrgN = 0.0;
envi.litterOrgN = 0.0;
envi.plantLeafN = 0.0;
envi.plantWoodN = 0.0;
envi.fineRootN = 0.0;
envi.coarseRootN = 0.0;
}

climate = firstClimate;
Expand Down
27 changes: 27 additions & 0 deletions src/sipnet/state.h
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,16 @@ typedef struct Parameters {
// Fraction of mineral N lost to leaching per day
double nLeachingFrac;

// C:N ratios for plant pools (g C per g N)
// C:N ratio of leaves
double leafCN;
// C:N ratio of wood
double woodCN;
// C:N ratio of fine roots
double fineRootCN;
// C:N ratio of coarse roots
double coarseRootCN;

} Params;

#define NUM_PARAMS (sizeof(Params) / sizeof(double))
Expand Down Expand Up @@ -417,6 +427,12 @@ typedef struct Environment {
double soilOrgN;
// litter organic nitrogen pool (g N m^-2 ground area)
double litterOrgN;

// plant nitrogen pools (g N m^-2 ground area)
double plantLeafN;
double plantWoodN;
double fineRootN;
double coarseRootN;
} Envi;

// Global var
Expand Down Expand Up @@ -546,6 +562,17 @@ typedef struct FluxVars {
double nVolatilization;
// Mineral N lost to leaching
double nLeaching;

// Plant N demand and uptake fluxes (g N * m^-2 ground area * day^-1)
// Total plant N demand
double plantNDemand;
// Actual N uptake by plants
double plantNUptake;
// N demand for each pool
double leafNDemand;
double woodNDemand;
double fineRootNDemand;
double coarseRootNDemand;

// ****************************************
// Fluxes for event handling
Expand Down
7 changes: 7 additions & 0 deletions tests/sipnet/test_events_infrastructure/events.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
year day type param_name=delta[,param_name=delta,...]
2023 65 plant fluxes.eventLeafC=6.00,fluxes.eventWoodC=8.00,fluxes.eventFineRootC=10.00,fluxes.eventCoarseRootC=12.00
2023 70 irrig fluxes.eventSoilWater=10.00,fluxes.eventEvap=0.00
2023 200 harv fluxes.eventLitterC=10.93,fluxes.eventLeafC=-11.86,fluxes.eventWoodC=-9.50,fluxes.eventFineRootC=-7.46,fluxes.eventCoarseRootC=-7.78
2024 65 plant fluxes.eventLeafC=6.00,fluxes.eventWoodC=10.00,fluxes.eventFineRootC=14.00,fluxes.eventCoarseRootC=18.00
2024 70 irrig fluxes.eventSoilWater=5.00,fluxes.eventEvap=5.00
2024 200 harv fluxes.eventLitterC=8.51,fluxes.eventLeafC=-2.78,fluxes.eventWoodC=-3.27,fluxes.eventFineRootC=-5.04,fluxes.eventCoarseRootC=-5.93
2 changes: 2 additions & 0 deletions tests/sipnet/test_events_types/events.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
2024 70 till eventTrackers.d_till_mod=0.50
2024 75 till eventTrackers.d_till_mod=0.20
1 change: 1 addition & 0 deletions tests/sipnet/test_modeling/events.out
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
2024 70 fert fluxes.eventOrgN=120.00,fluxes.eventLitterC=40.00,fluxes.eventMinN=80.00
Loading