This repository was archived by the owner on Oct 6, 2026. It is now read-only.
Repository navigation
Old River diversion: persist the last observation and keep the subtraction out of the routing state - #127
Merged
Conversation
sclaw
reviewed
Sep 15, 2026
sclaw
left a comment
There was a problem hiding this comment.
This looks good, Taher. Before I approve, could you please re-run test/nhf/old_river/run-troute-with-water-transfer.ipynb and paste the output figures? Also please respond to my two comments.
cheginit
force-pushed
the
feat/diversion-persistence
branch
from
September 16, 2026 17:08
0189e50 to
71b0f0a
Compare
Author
cheginit
force-pushed
the
feat/diversion-persistence
branch
2 times, most recently
from
September 17, 2026 12:55
cad2b8d to
bea040e
Compare
The kernel subtracts the observed diversion from the donor's stored outflow, which the next step reads back as qdp and carries through C3, so the donor lost d/(1-C3): 1.25 times the observation at Old River, where C3 is 0.2. The kernel now remembers what it removed per gage, restores it when loading qdp (as qdpp does for bottom-added qlat), and returns it as state: the DA object keeps the amount each donor gave up at its last step, both drivers hand it to the next window and the BMI checkpoint carries it, so a clamp or a missing observation at a window boundary routes exactly. Pinned with an analytic steady state, a one-step pulse, and windowed runs against a continuous one.
…y it A diversion gage with no observation fell into the kernel's lastobs decay, so the receiving river kept gaining water for two hours after the donor had stopped giving it. The kernel now takes the diversion gage set, keyed on the gage because the receiving node's job usually lacks the donor, and leaves such a gage at its routed flow.
The roster comes from the lastobs frame, so a lastobs_file that predates the diversion left its gage off it: the receiver was never nudged while the injected row still drove the donor subtraction. The diversion gages join the roster whenever they have observations, with an empty lastobs row.
The horizon idiom the other DAs use (reservoir_rfc_forecast_persist_days): whole days, default 11, 0 disables. Replaces persist_historical_median. Two donors on one gage are rejected, and a crosswalk without usgs_timeslices_folder warns that nothing will be diverted.
…ts gage without nudging The diversion reads its gage's row from usgs_timeslices_folder itself, as a named TimeSlice consumer in both build_da_sets copies (which now carry the window length), and extends the row onto the window's routing grid with the last observation held, causally, until diversion_persist_days after it. A seed carries the hold across windows and a report at the seed's instant wins. The scaling DA's merge, whose gage set includes the diversion gage with raw observations only, protects the diversion's rows in both drivers. The hardcoded monthly climatology and its fill are gone; a gage with nothing to divert is reported.
The seed is taken as of the checkpoint's t0, the restore keeps the later of the checkpoint's seed and the one init built, and re-runs the fill on the frame's own columns, since the rows were filled at init.
The merge reindexes every surviving row onto the window's columns, and nothing rebuilds the run-spanning rows between BMI windows, so a merge stored on the DA object handed every later window one column of the nudging and diversion rows read at init.
…none A forecast cycle started without a checkpoint reads only the reader's lookback before t0, so a gage quiet for longer than a day would divert nothing although the horizon allows eleven. The diversion scans the TimeSlice files newest first, back to the horizon, for a gage with no carried seed and no report in the frame, and holds its last report that the reader would accept. The hold's deadline is absolute, so hourly AnA cycles expire it at the same instant as one long window.
The gage record is cut a month before the run ends; the last value must hold at both nodes, with nudging on and, against a control with no DA at all, with nudging off.
All five ranges set usgs_timeslices_folder and the diversion_da block. The horizon covers each range plus 28 hours of data age: 11 days for the analysis and short ranges, 12 for medium range, 32 for long range.
A forecast window lists every future instant and none of them exists yet, so build_da_sets warned once per file, thousands of lines per long-range window. Both copies now name the count and the span once.
One configuration serves every domain, so a crosswalk entry the domain does not contain is a warning and no diversion there, not a failed build.
…ate root Seeded near zero depth the secant settles where C3 = 1 and the segment passes its own previous outflow whatever arrives. A residual above a tenth of the Muskingum estimate restarts the search from the flow's normal depths; cold-start transients change.
The kernel reads q0 by position as qu0, qd0, h0, ql0; the reader emitted qd0, h0, qu0, ql0, putting the flow in the depth slot.
…g link The write call had been commented out since the driver was created, and the reader took only flowpath-keyed files. A run restarted from the written file matches the uninterrupted run exactly.
The retrospective hot start carries neither the Vicksburg nudge nor the transfer, which reach Baton Rouge a day and four days apart; each case now restarts from its own spun-up state.
…ates The third run is the persistence case with the record cut at 2011-05-31.
…rs only A bare dropna() made every column a drop criterion, so a dam with a null native lake_id, or a frame carrying lakeout columns, lost its reservoir. Both gates now share LEVEL_POOL_PARAMS, the columns the kernel reads.
…list A parameter missing from LEVEL_POOL_PARAMS reaches the kernel as NaN and routes plausible wrong numbers; the kernel-facing column list is named in compute.py and the two are asserted equal.
Each expected behavior of the transfer checked with an asserted number on real inputs; helpers in notebook_utils.py, logs to a file.
pip resolved matplotlib's extras to numpy 2.5.3 over the 1.26.4 the project pins, so CI built and tested the kernel on a numpy the project does not target; the deprecation noise in the test log is pandas and netCDF4 under that numpy. The dev image installs the same way.
The schema dumps a section the configuration leaves out as None, and a configuration with only the diversion block crashed on `reservoir_da.get` at init and on `streamflow_da.get` at rollover.
cheginit
force-pushed
the
feat/diversion-persistence
branch
from
September 17, 2026 14:16
bea040e to
e31d880
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The Old River Control Structure diversion must assimilate observations and persist the
last one rather than decay it. Two base defects sat under it: the subtraction fed back
through the Muskingum-Cunge state, so the Mississippi lost 1.25 times the observation,
and when the gage record ended the transfer stopped within the hour while the
Atchafalaya decayed over two.
Additions
diversion_da.diversion_persist_days(whole days, default 11, 0 disables), the sameidiom as
reservoir_rfc_forecast_persist_days. The last observation is held flat onthe routing grid until an absolute deadline; after it both sides stop at once.
usgs_timeslices_folder(a named consumer inboth
build_da_setscopies); nothing depends onstreamflow_nudging. A process withno report at or before t0 and no checkpoint seed scans the folder back to the horizon,
once, and remembers an empty result.
job that routes the donor, carried to the next window by both drivers and by the BMI
checkpoint.
data age (12 days for medium range, 32 for long range).
every window when
output_parameters.lite_restartnames a directory, and the networkreads it back as it is (a file with a duplicated link is refused); the call had been
commented out since the driver was created.
The Old River demonstration spins each case up five days and restarts from that file;
a restarted run is bit-identical to the uninterrupted one.
Removals
persist_historical_medianand the hardcoded monthly means.Changes (base fixes)
qdp, asqdppdoes forbottom-added qlat.
decay (keyed on the gage; the receiver's job usually lacks the donor).
lastobs_filethat predates the diversion cannot silence the receiver.object it handed every later window one column of the run-spanning rows.
failed network build, so one configuration serves every domain.
diversion_daset, which the schema dumps with every otherDA section as
None, initializes and rolls over instead of crashing on those sections.h0, ql0); the reader emitted qd0, h0, qu0, ql0, which put the flow in the depth slot.
(secant residual above a tenth of the Muskingum estimate). A link handed a flow far
above its previous outflow used to publish a dry channel and pass about 100 cms per
step, which put five April dips at Simmesport in the demo; pinned by
test_mc_depth_seed.py, and the threetest_mc_nonconvergence.pycases that frozedepth at bankfull now solve to their normal depth. Ordinary channels are bit-identical
to
development; only collapsed searches differ: cold-start transients, and tricklesof a few centimeters over very wide beds (under 0.3 cms on a bed wider than 100 m).
Testing
pixi run pytest -m "not integration": 920 passed, 4 skipped, 1 xfailed; everyintegration test in the repository 7 passed, 6 skipped (unprepped domains, obsolete modules).
pytest test/nhf/old_river -m integration: 3 passed. Record cut 2011-05-31; bothnodes hold 14,484 cms for 30 days; the donor gives up 1.000 times the observation
(was 1.251). Runs with nudging on and, against a control with no DA, off. Simmesport moves less than
10 percent in any hour while the observation is applied or held.
cases identical under every compute method, pools 1 and 2, including a donor at a
subnetwork cut.
and the tests now start 2011-04-19 from restarts written by five-day spin-ups of each
case; the retrospective hot start carries neither the Vicksburg nudge nor the transfer
and put a two-day dip at Baton Rouge in every earlier figure.
test/nhf/old_river/old-river-diversion.ipynbchecks each expected behavior on realinputs with asserted numbers: the lateral inflow at the structure is at most 0.1 cms
against 17,000 to 66,000 routed; the amount removed at the donor and added at the
receiver equals the observation at every written hour; the transfer reaches Baton
Rouge and Simmesport with the same volume; the last value holds 45 days and the donor
stops at the horizon; a fourfold observation floors the donor at zero and the run logs
the mass imbalance while the Atchafalaya still receives the full amount. Run logs go
to a file, not the cells.
Notes
diversion_dais set, nudging on oroff, with station 07381482 in the files; every range's deployment stages it.
diversion_persist_daysfor a quiet gage, so the routing job stages that many days ofthe per-day
usgs_timeslicesdirectories into the folder. A scan that finds nothinglogs the folder's oldest file.
Checklist