Add Reservoir DA Functionality for NHF#109
Conversation
…VS Code from shell
…. conus_lakes test passing.
cheginit
left a comment
There was a problem hiding this comment.
Nice work on this. Getting reservoir DA wired into the NHF path is a meaningful step, and the overall shape is right. A few things I appreciated:
- Moving to
nhf_lake_idas the primary key is the correct call, and thereservoir_datable-driven approach — derivingreservoir_typefromda_typeand demoting to level pool when a DA flag is off — is clean and easy to follow. - The
_clean_waterbodiesrefactor is a real improvement: counting and warning on each dropped category instead of failing the whole run on the first bad row makes hydrofabric debugging much nicer. - Diagnosing that the large synthetic lake ids overflow int32 and widening the kernel indices to int64 was the right fix. I built and ran the four_lakes case, and once it's routing, all four DA sources land correctly (USGS/USACE/USBR persistence at their gage values and the RFC reservoir at its forecast), so the core logic is sound.
- Good instinct covering all four reservoir DA types in a dedicated test.
I did find a few things to address before merge. A couple of them are build/run blockers that only surface on a clean build (modern gcc/clang) and on actually exercising the reservoir-DA path, plus the Great Lakes interaction with the id rename. Details in the inline comments below.
| * | ||
| */ | ||
| void init_levelpool_reach(_Reach* reach, int lake_number, | ||
| void init_levelpool_reach(_Reach* reach, long lake_number, |
There was a problem hiding this comment.
Building this on a modern toolchain (gcc >= 14 / clang >= 16) fails to compile:
levelpool_structs.c: in function 'init_levelpool_reach':
error: passing argument 12 of 'init_lp' from incompatible pointer type [-Wincompatible-pointer-types]
The int -> long widening of lake_number reaches the C structs and the Cython layer, but not the Fortran kernels: bind_lp.f90, bind_hybrid.f90, and bind_rfc.f90 still declare integer, intent(in) :: lake_number (4-byte c_int), while init_levelpool_reach / init_hybrid_reach / init_rfc_reach now pass &lake_number as a long*. No .f90 is touched in this PR.
On older compilers this is just a warning, which is why local builds pass but then Fortran reads 4 bytes of the 8-byte long, so a large nhf_lake_id truncates inside the kernel. On gcc >= 14 / clang >= 16 it's a hard error and the package won't build.
Fix: declare lake_number as integer(c_int64_t) (ISO_C_BINDING) in the three binds and propagate through the module init methods they call (plus any derived-type field that stores it).
Same issue at hybrid_structs.c and rfc_structs.c
There was a problem hiding this comment.
Nice find! I believe I have resolved this.
There was a problem hiding this comment.
One thing to finish so the id widening is fully platform-agnostic: lake_number and the reservoir index arrays are currently a mix of long and int64_t, which only holds together on Linux.
long isn't a portable 64-bit type. It's 64-bit on Linux and macOS (LP64) but 32-bit on Windows (LLP64). And even among 64-bit platforms, int64_t resolves to a different underlying type — it's long on Linux but long long on macOS — so there long and int64_t are distinct C types. The Fortran binds and the init_* externs in the *_structs.c files use int64_t (C_INT64_T), while the C struct fields, the init_*_reach signatures, and the Cython side still use long. That mixed state compiles on Linux (where int64_t == long) but not elsewhere: on macOS, passing &reach->reach.lp.lake_number (a long*) into an int64_t* parameter is an incompatible pointer type; on Windows, long is too narrow to hold the id at all (truncation).
Using int64_t uniformly for every 64-bit id avoids all of it. Changes needed:
levelpool_structs.h,hybrid_structs.h,rfc_structs.h: struct fieldlong lake_number;→int64_t lake_number;, and add#include <stdint.h>.levelpool_structs.c,hybrid_structs.c,rfc_structs.c: theinit_*_reach(...)definition'slong lake_number→int64_t(theint64_t *lake_numberexterns stay as-is).reach.pxdand the three reservoir.pxd:long lake_number→int64_t lake_numberin the_MC_*struct decls, plusfrom libc.stdint cimport int64_tat the top.levelpool.pyx,hybrid.pyx,rfc.pyx:long lake_number→int64_tin both theinit_*_reachextern block and the__init__signature, plus the same cimport.mc_reach.pyx: the fourreservoir_*_wbody_idxmemoryviews (const long[:]) and theusgs_idx/usace_idx/usbr_idx/rfc_idxarrays (np.ndarray[long]) →int64_t, plus the cimport. compute.py already builds these asint64, so this makes the buffer binding valid on every platform and along[:]view can't bind anint64array wherelongis 32-bit.
The same long → int64_t treatment applies to any other field carrying a 64-bit id (e.g. the reach id and upstream_ids in the reservoir __init__s) if you want it fully airtight on Windows as well.
…handle the string IDs.
|
@cheginit , I have updated this. Please review at your convenience. Tests passing
|
…raised. Add plots to give visual reassurance that the runs produced good results. Delete old outputs before tests run
…or of catch-all ciss creek
|
The following tests are passing pytest -m integration
python benchmark/run_conus_lakes.py
benchmark/regression_check.sh |
|
@cheginit , apologies for the scope creep on this PR. I have added the following features to this PR Additions
Removals
Changes
TestingAs stated above, the following tests are passing for me pytest -m integration
python benchmark/run_conus_lakes.py
benchmark/regression_check.sh |
This PR adds the option to use data assimilation on reservoirs with an NHF network.
Additions
preprocess_data_assimilation()method innhf_preprocess.py. Modified hyfeaturesnetwork approach to improve readability. Much of the hyfeaturesnetwork functionality was moved to nhf_builds, so the table was more or less a simple load and store operation.Removals
Changes
make_forcing.py. Not scoped within this story, but was a useful fixTesting
test/nhf/four_lakes/test_reservoir_da.pythat checks whether reservoir data will be assimilated from USGS, USACE, RFC, and USBR sources. The test adds large qlats to every reach and then artificially sets USGS, USACE, and USBR gages at a low value. At first, I attempted to do 0 qlat and force reservoir outflow high, but the kernel guarded against negative storage and would not allow this. RFC data is set high because the RFC method can force an outflow directly instead of using the hybrid DA approach.regression_check.shWith the modifications to compiled kernel code, I had to manually modify this test as follows.
bench_kernel.pyregression_check.shNotes