From cd847900addb18ce54eef40c653476de035718d6 Mon Sep 17 00:00:00 2001 From: Gaurav Chaudhary Date: Wed, 28 Oct 2026 22:09:55 +0530 Subject: [PATCH] [#421] Fix missed events across year boundaries --- docs/CHANGELOG.md | 1 + src/sipnet/events.c | 10 +- .../test_events_infrastructure/Makefile | 2 +- .../testEventDateChecks.c | 122 ++++++++++++++++++ 4 files changed, 131 insertions(+), 4 deletions(-) create mode 100644 tests/sipnet/test_events_infrastructure/testEventDateChecks.c diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 0a7d73241..ec53f2330 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -32,6 +32,7 @@ sections to include in release notes: fertilizer events, NPP signs, nitrogen availability, and flooding. (#415) ### Fixed +- Report missed agronomic events immediately across year boundaries instead of silently blocking later events. (#421) - Restore format specifiers for stock value in `ensureNonNegative()` error logging to align arguments and prevent undefined behavior. (#406) - Terminating harvest events will now remove all biomass at end of time step, instead of allowing small amounts to remain in the plant pools due to growth in that step. (#400) - Leaf-off mechanics refined to ensure that leaf-off events do not remove more leaf carbon than is available in the leaf pool. (#400) diff --git a/src/sipnet/events.c b/src/sipnet/events.c index 3d05db574..65cfa8b08 100644 --- a/src/sipnet/events.c +++ b/src/sipnet/events.c @@ -490,6 +490,10 @@ void writePendingHarvestEvent(EventNode *event) { } } +static int eventIsDue(const EventNode *event, int year, int day) { + return event->year < year || (event->year == year && event->day <= day); +} + void writeEventsOut(void) { // We move the global events pointer here (instead of a copy as in the // processEvents functions), as this is the last pass @@ -501,7 +505,7 @@ void writeEventsOut(void) { int hasSeenLeafOn = 0; int hasSeenLeafOff = 0; int hasSeenHarvest = 0; - while (gEvent != NULL && gEvent->year <= climYear && gEvent->day <= climDay) { + while (gEvent != NULL && eventIsDue(gEvent, climYear, climDay)) { // First up, check the pending events if (gEvent->type == HARVEST) { if (hasSeenHarvest) { @@ -638,7 +642,7 @@ void processEventsForCarbon(EventNode *event) { int hasSeenLeafOff = 0; int hasSeenHarvest = 0; - while (event != NULL && event->year <= climYear && event->day <= climDay) { + while (event != NULL && eventIsDue(event, climYear, climDay)) { // The events file has been tested on read, so we know this event list // should be in chrono order. However, we need to check to make sure the // current event is not in the past, as that would indicate an event that @@ -905,7 +909,7 @@ void processEventsForNitrogen(EventNode *event) { int hasSeenLeafOff = 0; int hasSeenHarvest = 0; - while (event != NULL && event->year <= climYear && event->day <= climDay) { + while (event != NULL && eventIsDue(event, climYear, climDay)) { switch (event->type) { case IRRIGATION: { // Nothing to do for irrigation events diff --git a/tests/sipnet/test_events_infrastructure/Makefile b/tests/sipnet/test_events_infrastructure/Makefile index 0da7bed64..d0610aa4f 100644 --- a/tests/sipnet/test_events_infrastructure/Makefile +++ b/tests/sipnet/test_events_infrastructure/Makefile @@ -8,7 +8,7 @@ LDFLAGS=-L$(ROOT_DIR)/libs LDLIBS=-lsipnet -lsipnet_common -lm # List test files in this directory here -TEST_CFILES=testEventInfra.c testEventInfraNeg.c testEventOutputFile.c +TEST_CFILES=testEventInfra.c testEventInfraNeg.c testEventOutputFile.c testEventDateChecks.c # The rest is boilerplate, likely copyable as is to a new test directory TEST_OBJ_FILES=$(TEST_CFILES:%.c=%.o) diff --git a/tests/sipnet/test_events_infrastructure/testEventDateChecks.c b/tests/sipnet/test_events_infrastructure/testEventDateChecks.c new file mode 100644 index 000000000..960a67b5e --- /dev/null +++ b/tests/sipnet/test_events_infrastructure/testEventDateChecks.c @@ -0,0 +1,122 @@ +#include "utils/tUtils.h" +#include "common/context.h" +#include "sipnet/events.c" +#include "utils/exitHandler.c" + +static ClimateNode testClimate; + +static void resetTest(void) { + freeEventList(); + gEvents = NULL; + gEvent = NULL; + fluxes = (Fluxes){0}; + initEventTrackers(); + testClimate = (ClimateNode){.year = 2016, .day = 100, .length = 1.0}; + climate = &testClimate; +} + +static int testMissedEvent(int year, int day, int currentYear, int currentDay) { + resetTest(); + gEvents = createEventNode(year, day, TILLAGE, "0.2"); + gEvents->nextEvent = createEventNode(currentYear, currentDay, TILLAGE, "0.3"); + setupEvents(); + // The first climate record precedes both events. + processEventsForCarbon(gEvent); + climate->year = currentYear; + climate->day = currentDay; + + should_exit = 1; + expected_code = EXIT_CODE_INPUT_FILE_ERROR; + exit_result = 1; + int jumped = setjmp(jump_env); + if (!jumped) { + processEventsForCarbon(gEvent); + } + test_assert(jumped == 1); + test_assert(eventTrackers.d_till_mod == 0.0); + test_assert(gEvent == gEvents); + if (!exit_result) { + logTest("Missed event %d/%d at climate %d/%d was not rejected\n", year, day, + currentYear, currentDay); + } + return !exit_result; +} + +static void processDate(int year, int day) { + climate->year = year; + climate->day = day; + fluxes = (Fluxes){0}; + processEventsForCarbon(gEvent); + processEventsForNitrogen(gEvent); + writeEventsOut(); +} + +static int testValidSchedule(int nitrogen) { + resetTest(); + updateIntContext("nitrogenCycle", nitrogen, CTX_TEST); + updateIntContext("litterPool", 1, CTX_TEST); + gEvents = createEventNode(2016, 200, TILLAGE, "0.2"); + EventNode *nextYear = createEventNode(2017, 10, TILLAGE, "0.3"); + gEvents->nextEvent = nextYear; + nextYear->nextEvent = createEventNode(2017, 10, FERTILIZATION, "2 3 4"); + setupEvents(); + eventOutFile = tmpfile(); + if (!eventOutFile) { + logTest("Unable to create temporary event output\n"); + return 1; + } + + // Any unexpected exit in the successful schedule must fail the executable. + really_exit = 1; + int status = 0; + processDate(2016, 100); + status |= gEvent != gEvents || eventTrackers.d_till_mod != 0.0; + processDate(2016, 200); + status |= gEvent != nextYear; + status |= !compareDoubles(eventTrackers.d_till_mod, 0.2); + // A lower day-of-year in a future year must not be processed early. + processDate(2016, 250); + status |= gEvent != nextYear || fluxes.eventInputN != 0.0; + processDate(2017, 10); + status |= gEvent != NULL; + status |= !compareDoubles(eventTrackers.d_till_mod, 0.5); + status |= !compareDoubles(fluxes.eventLitterC, 3.0); + status |= !compareDoubles(fluxes.eventInputN, nitrogen ? 6.0 : 0.0); + // A second climate record that day must not apply any event again. + processDate(2017, 10); + status |= !compareDoubles(eventTrackers.d_till_mod, 0.5); + status |= fluxes.eventLitterC != 0.0 || fluxes.eventInputN != 0.0; + processDate(2017, 11); // empty queue + + rewind(eventOutFile); + int year, day; + char line[1024], type[32]; + int rows = 0; + while (fgets(line, sizeof(line), eventOutFile)) { + if (sscanf(line, "%d %d %31s", &year, &day, type) != 3) { + status = 1; + continue; + } + status |= year != (rows == 0 ? 2016 : 2017); + status |= day != (rows == 0 ? 200 : 10); + status |= strcmp(type, rows == 2 ? "fert" : "till") != 0; + rows++; + } + status |= rows != 3; + closeEventOutFile(); + return status; +} + +int main(void) { + initContext(); + updateIntContext("events", 1, CTX_TEST); + int status = testMissedEvent(2016, 200, 2017, 10); + status |= testMissedEvent(2016, 200, 2017, 200); + status |= testMissedEvent(2016, 200, 2017, 250); + status |= testMissedEvent(2016, 200, 2016, 210); + status |= testValidSchedule(0); + status |= testValidSchedule(1); + resetTest(); + logTest("%s testEventDateChecks\n", status ? "FAILED" : "PASSED"); + return status; +}