-
Notifications
You must be signed in to change notification settings - Fork 29
SIP328 fix wood storage bookkeeping so handling of plantWoodCStorageDelta is consistent
#330
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -479,15 +479,18 @@ void processEvents(void) { | |
| const double fracRB = harvParams->fractionRemovedBelow; | ||
| const double fracTB = harvParams->fractionTransferredBelow; | ||
|
|
||
| const double woodC = envi.plantWoodC + envi.plantWoodCStorageDelta; | ||
| const double aboveHarvestFrac = fracRA + fracTA; | ||
| const double storageWoodC = fmax(0.0, envi.plantWoodCStorageDelta); | ||
| const double woodC = getStorageBackedWoodCarbon(); | ||
| // Litter increase | ||
| double litterAdd = fracTA * (envi.plantLeafC + woodC); | ||
| double soilAdd = fracTB * (envi.fineRootC + envi.coarseRootC); | ||
|
|
||
| // Pool reductions, counting both mass moved to litter and removed by | ||
| // the harvest itself. Above-ground changes: | ||
| const double leafDelta = -envi.plantLeafC * (fracRA + fracTA); | ||
| const double woodDelta = -woodC * (fracRA + fracTA); | ||
| const double leafDelta = -envi.plantLeafC * aboveHarvestFrac; | ||
| const double woodDelta = -envi.plantWoodC * aboveHarvestFrac; | ||
| const double storageWoodDelta = -storageWoodC * aboveHarvestFrac; | ||
| // Below-ground changes: | ||
| const double fineDelta = -envi.fineRootC * (fracRB + fracTB); | ||
| const double coarseDelta = -envi.coarseRootC * (fracRB + fracTB); | ||
|
|
@@ -502,6 +505,7 @@ void processEvents(void) { | |
| fluxes.eventSoilC += soilAdd / climLen; | ||
| fluxes.eventLeafC += leafDelta / climLen; | ||
| fluxes.eventWoodC += woodDelta / climLen; | ||
| fluxes.eventWoodStorageDelta += storageWoodDelta / climLen; | ||
| fluxes.eventFineRootC += fineDelta / climLen; | ||
|
Comment on lines
506
to
509
|
||
| fluxes.eventCoarseRootC += coarseDelta / climLen; | ||
|
|
||
|
|
@@ -601,6 +605,7 @@ void updatePoolsForEvents(void) { | |
| // CARBON | ||
| // Harvest and planting events | ||
| envi.plantWoodC += fluxes.eventWoodC * climate->length; | ||
| envi.plantWoodCStorageDelta += fluxes.eventWoodStorageDelta * climate->length; | ||
| envi.plantLeafC += fluxes.eventLeafC * climate->length; | ||
|
|
||
| // Harvest and fertilization events | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,7 @@ | ||
| // Definition of global state variables | ||
|
|
||
| #include <math.h> | ||
|
|
||
| #include "state.h" | ||
|
|
||
| // linked list of climate variables | ||
|
|
@@ -12,3 +14,11 @@ Envi envi; // state variables | |
| Trackers trackers; | ||
| PhenologyTrackers phenologyTrackers; | ||
| Fluxes fluxes; | ||
|
|
||
| double getPlantWoodCTotal(void) { | ||
| return envi.plantWoodC + envi.plantWoodCStorageDelta; | ||
| } | ||
|
|
||
| double getStorageBackedWoodCarbon(void) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ca you explain the name here? I'm not following what "storage backed" is supposed to mean. |
||
| return fmax(envi.plantWoodC, getPlantWoodCTotal()); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,260 @@ | ||
| #include <math.h> | ||
| #include <stdio.h> | ||
| #include <stdlib.h> | ||
| #include <string.h> | ||
|
|
||
| #include "common/logging.h" | ||
| #include "utils/tUtils.h" | ||
|
|
||
| #include "sipnet/events.c" | ||
| #include "sipnet/sipnet.c" | ||
|
|
||
| #define TEST_EPS 1e-8 | ||
| #define OUTPUT_FILE "wood_storage.out" | ||
| #define HARVEST_FILE "storage_harvest.in" | ||
|
|
||
| static ClimateNode testClimate; | ||
|
|
||
| static int checkClose(const char *label, double actual, double expected) { | ||
| if (fabs(actual - expected) <= TEST_EPS) { | ||
| return 0; | ||
| } | ||
|
|
||
| logTest("%s is %f, expected %f\n", label, actual, expected); | ||
| return 1; | ||
| } | ||
|
|
||
| static void initStorageTestState(void) { | ||
| initContext(); | ||
| updateIntContext("events", 1, CTX_TEST); | ||
|
|
||
| testClimate = (ClimateNode){0}; | ||
| testClimate.year = 2026; | ||
| testClimate.day = 120; | ||
| testClimate.time = 12.0; | ||
| testClimate.length = 1.0; | ||
| testClimate.tair = 0.0; | ||
| testClimate.tsoil = 5.0; | ||
| climate = &testClimate; | ||
| firstClimate = &testClimate; | ||
|
|
||
| if (meanNPP == NULL) { | ||
| meanNPP = newMeanTracker(0.0, MEAN_NPP_DAYS, MEAN_NPP_MAX_ENTRIES); | ||
| } else { | ||
| resetMeanTracker(meanNPP, 0.0); | ||
| } | ||
|
|
||
| params.baseVegResp = 1.0; | ||
| params.vegRespQ10 = 1.0; | ||
| params.woodTurnoverRate = 0.5; | ||
| params.coarseRootTurnoverRate = 0.0; | ||
| params.fineRootTurnoverRate = 0.0; | ||
| params.coarseRootAllocation = 0.0; | ||
| params.fineRootAllocation = 0.0; | ||
| params.woodAllocation = 0.0; | ||
| params.leafCN = 20.0; | ||
| params.woodCN = 10.0; | ||
| params.fineRootCN = 30.0; | ||
|
|
||
| envi = (Envi){0}; | ||
| envi.soilC = 10.0; | ||
| envi.soilWater = 5.0; | ||
|
|
||
| initTrackers(); | ||
| resetFluxes(); | ||
| initBalanceTracker(); | ||
| } | ||
|
Comment on lines
+47
to
+66
|
||
|
|
||
| static int getOutputValue(const char *name, double *value) { | ||
| FILE *out = fopen(OUTPUT_FILE, "w"); | ||
| if (out == NULL) { | ||
| logTest("Unable to open %s for writing\n", OUTPUT_FILE); | ||
| return 1; | ||
| } | ||
|
|
||
| outputHeader(out); | ||
| outputState(out, testClimate.year, testClimate.day, testClimate.time); | ||
| fclose(out); | ||
|
|
||
| out = fopen(OUTPUT_FILE, "r"); | ||
| if (out == NULL) { | ||
| logTest("Unable to open %s for reading\n", OUTPUT_FILE); | ||
| return 1; | ||
| } | ||
|
|
||
| char header[4096]; | ||
| char values[4096]; | ||
| if (fgets(header, sizeof(header), out) == NULL || | ||
| fgets(values, sizeof(values), out) == NULL) { | ||
| fclose(out); | ||
| logTest("Unable to read output lines from %s\n", OUTPUT_FILE); | ||
| return 1; | ||
| } | ||
| fclose(out); | ||
|
|
||
| int targetIndex = -1; | ||
| int index = 0; | ||
| char *headerToken = strtok(header, " \t\n"); | ||
| while (headerToken != NULL) { | ||
| if (strcmp(headerToken, name) == 0) { | ||
| targetIndex = index; | ||
| break; | ||
| } | ||
| index++; | ||
| headerToken = strtok(NULL, " \t\n"); | ||
| } | ||
| if (targetIndex < 0) { | ||
| logTest("Unable to find output column %s\n", name); | ||
| return 1; | ||
| } | ||
|
|
||
| index = 0; | ||
| char *valueToken = strtok(values, " \t\n"); | ||
| while (valueToken != NULL) { | ||
| if (index == targetIndex) { | ||
| *value = atof(valueToken); | ||
| return 0; | ||
| } | ||
| index++; | ||
| valueToken = strtok(NULL, " \t\n"); | ||
| } | ||
|
|
||
| logTest("Unable to find value for output column %s\n", name); | ||
| return 1; | ||
| } | ||
|
|
||
| static int testOutputSemantics(void) { | ||
| int status = 0; | ||
| double reportedWood = 0.0; | ||
| double storage = 0.0; | ||
|
|
||
| logTest("Starting testOutputSemantics()\n"); | ||
| initStorageTestState(); | ||
|
|
||
| envi.plantWoodC = 100.0; | ||
| envi.plantWoodCStorageDelta = 25.0; | ||
| envi.plantLeafC = 5.0; | ||
|
|
||
| status |= getOutputValue("plantWoodC", &reportedWood); | ||
| status |= getOutputValue("nppStorage", &storage); | ||
| status |= checkClose("reported plantWoodC", reportedWood, 125.0); | ||
| status |= checkClose("nppStorage", storage, 25.0); | ||
|
|
||
| return status; | ||
| } | ||
|
|
||
| static int testNegativeStorageDoesNotReduceWoodFluxes(void) { | ||
| int status = 0; | ||
| double folResp = 0.0; | ||
| double woodResp = 0.0; | ||
|
|
||
| logTest("Starting testNegativeStorageDoesNotReduceWoodFluxes()\n"); | ||
| initStorageTestState(); | ||
|
|
||
| envi.plantWoodC = 3.0; | ||
| envi.plantWoodCStorageDelta = -5.0; | ||
|
|
||
| vegResp(&folResp, &woodResp, 0.0); | ||
| calcRootAndWoodFluxes(); | ||
|
|
||
| status |= checkClose("woodResp", woodResp, 3.0); | ||
| status |= checkClose("woodLitter", fluxes.woodLitter, 1.5); | ||
| status |= checkClose("storage-backed wood carbon", | ||
| getStorageBackedWoodCarbon(), 3.0); | ||
|
|
||
| return status; | ||
| } | ||
|
|
||
| static int writeHarvestEventFile(void) { | ||
| FILE *events = fopen(HARVEST_FILE, "w"); | ||
| if (events == NULL) { | ||
| logTest("Unable to open %s for writing\n", HARVEST_FILE); | ||
| return 1; | ||
| } | ||
|
|
||
| fprintf(events, "%d %d harv 1 0 0 0\n", testClimate.year, testClimate.day); | ||
| fclose(events); | ||
| return 0; | ||
| } | ||
|
|
||
| static void runHarvestEvent(void) { | ||
| initEvents(HARVEST_FILE, "events.out", 0); | ||
| setupEvents(); | ||
| processEvents(); | ||
| updateBalanceTrackerPreUpdate(); | ||
| updatePoolsForEvents(); | ||
| updateBalanceTrackerPostUpdate(); | ||
| ensureNonNegativeStocks(); | ||
| updateBalanceTrackerPostClamp(); | ||
| checkBalance(); | ||
| closeEventOutFile(); | ||
| } | ||
|
|
||
| static int testHarvestRemovesPositiveStorage(void) { | ||
| int status = 0; | ||
|
|
||
| logTest("Starting testHarvestRemovesPositiveStorage()\n"); | ||
| initStorageTestState(); | ||
| status |= writeHarvestEventFile(); | ||
|
|
||
| envi.plantWoodC = 10.0; | ||
| envi.plantWoodCStorageDelta = 5.0; | ||
| envi.plantLeafC = 2.0; | ||
|
|
||
| runHarvestEvent(); | ||
|
|
||
| status |= checkClose("plantWoodC", envi.plantWoodC, 0.0); | ||
| status |= | ||
| checkClose("plantWoodCStorageDelta", envi.plantWoodCStorageDelta, 0.0); | ||
| status |= checkClose("eventWoodC", fluxes.eventWoodC, -10.0); | ||
| status |= | ||
| checkClose("eventWoodStorageDelta", fluxes.eventWoodStorageDelta, -5.0); | ||
| status |= checkClose("eventOutputC", fluxes.eventOutputC, 17.0); | ||
| status |= checkClose("deltaC", balanceTracker.deltaC, 0.0); | ||
|
|
||
| return status; | ||
| } | ||
|
|
||
| static int testHarvestWithNegativeStorageLeavesDebtVisible(void) { | ||
| int status = 0; | ||
|
|
||
| logTest("Starting testHarvestWithNegativeStorageLeavesDebtVisible()\n"); | ||
| initStorageTestState(); | ||
| status |= writeHarvestEventFile(); | ||
|
|
||
| envi.plantWoodC = 10.0; | ||
| envi.plantWoodCStorageDelta = -15.0; | ||
| envi.plantLeafC = 2.0; | ||
|
|
||
| runHarvestEvent(); | ||
|
|
||
| status |= checkClose("plantWoodC", envi.plantWoodC, 0.0); | ||
| status |= | ||
| checkClose("plantWoodCStorageDelta", envi.plantWoodCStorageDelta, -15.0); | ||
| status |= checkClose("total plantWoodC", getPlantWoodCTotal(), -15.0); | ||
| status |= checkClose("eventWoodC", fluxes.eventWoodC, -10.0); | ||
| status |= | ||
| checkClose("eventWoodStorageDelta", fluxes.eventWoodStorageDelta, 0.0); | ||
| status |= checkClose("eventOutputC", fluxes.eventOutputC, 12.0); | ||
| status |= checkClose("deltaC", balanceTracker.deltaC, 0.0); | ||
|
|
||
| return status; | ||
| } | ||
|
|
||
| int main(void) { | ||
| int status = 0; | ||
|
|
||
| logTest("Starting testWoodStorage\n"); | ||
|
|
||
| status |= testOutputSemantics(); | ||
| status |= testNegativeStorageDoesNotReduceWoodFluxes(); | ||
| status |= testHarvestRemovesPositiveStorage(); | ||
| status |= testHarvestWithNegativeStorageLeavesDebtVisible(); | ||
|
|
||
| if (status) { | ||
| logTest("FAILED testWoodStorage with status %d\n", status); | ||
| exit(status); | ||
| } | ||
|
|
||
| logTest("PASSED testWoodStorage\n"); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fluxes.eventWoodStorageDeltais introduced/updated for harvest, butresetEventFluxes()doesn’t currently reset it. IfprocessEvents()is ever called without a precedingresetFluxes()(several unit tests callprocessEvents()directly), the storage-delta event flux can carry over between timesteps/tests and corrupt pool updates. Addfluxes.eventWoodStorageDelta = 0.0;toresetEventFluxes()alongside the other event flux resets.