Updating workflows for reservoirs and rivers - #54
Merged
Conversation
- Changed order of renaming in PLD in case users specify "lake_id" as their id as well - Open config with utf-8 encoding to avoid bugs when editing .yaml files with Windows - Path normalization (function in "general" + fixes to project.py) to avoid path issues when opening nc files with hdf5/netcdf4 on Windows (breaks subsetting of sentinel files) - fixed bug in geometry with coords.pop function set as index
- Kalman epoch grouping + closed-form update - svr_radial_max_iter / svr_linear_max_iter separation: linear can be capped to 5000, radial needs longer run - Extraction skip-if-exists + Kalman grouping performance: allow overwrite = False to avoid re-reading all individual files, Kalman speed up - Bias correction tuning (relative_orbit, bias_group_by="platform_orbit") = Group by platform and by relative orbit Spatial correction tools (distance_penalty, spatial_correction_model) = Bias correction tools for tracks far a part - off by default Config wiring (merging_options layering, mission_options whitelist) Rivers: - SWORD extraction pipeline (core) - Sentinel-6 EarthData download (for HR data) - Plotting fixes and Misc bug fixes (stale warning, rivers.yaml) for river processing - Possibility to exclude tracks - Use SWORD river width to buffer along river line when extracting other missions - Handle multipolygon extraction - Clean up of processing files when re-running - Reach slope correction for river VS.
Update to new functionalities for rivers
Update rivers config file
Import "name as name" in flows.__init__.py
- Updated readme, agents, citation to accurately cite DAHITI
Contributor
Author
|
Updated to pass tests and with adequate documentation. |
KittelC
marked this pull request as ready for review
July 15, 2026 14:07
KittelC
marked this pull request as draft
July 15, 2026 14:08
Updated documentation, review of flow.py decomposition Fixed unary_union depreciation warning Replace unary_union with union_all(), fixed depreciation warning.
KittelC
marked this pull request as ready for review
July 16, 2026 12:18
- Find lake using "intersects" instead of "within" when comparing to PLD (more robust if using buffered / imprecise polygon) - Fixed potential error if not initializing first and if reservoir in CRS (reprojection order) - Added hydroweb test - Stop download if no lake in PLD.
USe more robust Light sqlight SWORD file to find lake_id and backfill with regional file. Was using random sqlite from unzipped folder, which could lead to lake silently not being found because looking at wrong file.
kzalite
reviewed
Jul 17, 2026
kzalite
left a comment
Contributor
There was a problem hiding this comment.
Very nice refactor and update!
I left some small comments, nothing critical.
| product, | ||
| { | ||
| "processing_filters": ["elevation", "MAD"], | ||
| "elevation_min_m": 0.0, |
Contributor
There was a problem hiding this comment.
Since these default values are repeated (min_m, max_m, mad_threshold), maybe it's worth setting them as CONSTANTS in the _constants file or at the top of the file?
| prj.mission_options.get("swot", {}) | ||
| .get("quality_filters", {}) | ||
| .get("nodes", {}) | ||
| .get("max_q", 2) |
Contributor
There was a problem hiding this comment.
Same: maybe extract as constant?
| logger.info("Kept raw SWORD zip file at %s (keep_raw_sword=True)", zip_path) | ||
|
|
||
|
|
||
| # ============================================================================ |
Contributor
There was a problem hiding this comment.
Remove these commented lines
| gdf.to_file(os.path.join(dst_dir, "swot.gpkg"), driver="GPKG") | ||
|
|
||
|
|
||
| # ============================================================================ |
| self.reservoirs.export_to_dfs0 = self.config["reservoirs"].get( | ||
| "export_to_dfs0", False | ||
| ) | ||
| # NOTE: this was previously read via getattr(prj.reservoirs, |
Contributor
There was a problem hiding this comment.
This comment can be removed
Comparison of lake / res id failed due to str / int mismatch. Converted to int in _reser_voir_init.
SWOT fixes and delete unnecessary comments Comparison of lake / res id failed due to str / int mismatch. Converted to int in _reser_voir_init.
CLeaned up where and how default processing parameters are defined (elev_min and max, and max_threshold) - previously defined 3 places and hardcoded in function, now called as constant and defined only in 1 place unless provided by users.
Contributor
Author
|
Reviewed comments above :
|
- Check overlap in area - proximity allowance now an area overlap (in % not meters). - currently backward compatibility, should be added.
Contributor
Author
|
Found bug in SWORD / AOI comparison:
|
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 join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
1. Kalman filter correctness + performance
Files:
basic_filters.py,timeseries.py_update(): for then=1case (only case used in this codebase), replaced the general matrix
pinv-based Kalman update with a direct precision-weighted-averageformula. Verified mathematically identical via 2000-trial fuzz test.
kalman()was grouping by exact time stamp, not calendar day, causing visiblewithin-day convergence artifacts on dense passes. Fixed to group by
date.floor("D"). Confirmed on real data: one case went from std0.432m (wrong, from hundreds of sequential single-point updates) to a
single clean value matching the raw data's actual range.
2.
svr_radial_max_iter/svr_linear_max_iterseparationFiles:
basic_filters.py,timeseries.pysvr_linearcapped to 5000 iterations for speed.svr_radial's (RBF kernel, highC)max_iterwas tried onceand found actively unsafe: output quality was non-monotonic in
max_iter(5000→10/5760 kept, 50000→827/5760, only unbounded→correct2154/5760). Reverted to
-1(unbounded) as the correct default.svr_radial_max_iterandsvr_linear_max_iterare genuinely separate everywhere, consistentlynamed.
3. Extraction skip-if-exists + Kalman grouping performance
Files:
basic_filters.py,flows.py,project.pykalman()performance improved by grouping onceupfront into a dict.
_extract_reservoirs_timeseriesre-extracting every raw file on every run regardless of whether output
already existed. Added
overwrite_extraction(defaultFalse) toskip already-extracted targets, implemented separately for ICESat-2,
Sentinel-3/6, and SWOT.
overwrite_extractionwas read viagetattr(..., False)inflows.pybut nothing ever set it fromconfig — silently always
False. Fixed inproject.pyfor bothreservoirs and rivers.
4. Bias correction tuning + config wiring
Files:
flows.py,project.pystable key for Sentinel-3 - Sentinel-6 still uses
"pass"as
orbit_key, flagged as unverified whether it has the sameinstability or its own
relative_orbit-equivalent.bias_time_bin/bias_min_overlapwidened from10D/3to20D/1after confirming the stricter values were silently dropping entire
missions for reservoirs with sparse revisit patterns.
merging_optionsconfig layering bug fixed: the original lookup wasall-or-nothing (
getattr(...) or DEFAULTS), so setting any overridewould silently discard every other default.
mission_optionswhitelist extended forsigma0_min,source,latency,short_nameas each was needed.5. Exclusion / run_config system
Files:
flows.py,project.pyNew feature: per-target ability to interactively review and exclude
specific passes/platforms/dates from a merge, with the result persisted
in a way that's simultaneously a log, a config, and something hand-editable.
{output}/{id}/run_config.yaml: one file per target, holdingexclusions(a list of platform/orbit/date rules) andmerging_option_overrides(per-target parameter overrides). Writtenand read by the same functions regardless of whether the change came
from a notebook call or a hand edit — no separate "notebook state" to
keep in sync with "the config".
flows.py, exposed viaProjectmethods so they'rereachable without importing
flowsdirectly):list_target_observations(prj, target_type, id)— summarizesexisting observations by (platform, orbit), for reviewing what
could be excluded.
exclude_from_target(prj, target_type, id, platform=, orbit=, date=, reason=)— adds an exclusion rule at any granularity/combination.
list_exclusions/remove_exclusion(by index or by matchingcriteria — e.g.
platform="S3B", orbit=1517— added after initialreview found index-only removal asymmetric with how exclusions are
added).
set_merging_option(prj, target_type, id, **kwargs)— per-targetparameter overrides, same persistence.
Project._infer_target_type()— auto-resolves reservoirs vs. riverswhen a project only configures one of them; requires an explicit
target_typeargument only when both are configured._merge_timeseries: exclusions filter the concatenateddataframe before any bias_correct/Kalman/svr_radial processing.
merging_option_overridesis the highest-priority layer, aboveproject-wide config, above hardcoded defaults.
spatial-correction-relevant option via
set_merging_option, deletesthe cached
spatial_correction_model.json(and, after section 12 wasadded,
reach_slope_correction_model.jsontoo) — forcing a fresh fit,since the model may have been built from data that's no longer
included.
6. Width-based river buffer + multipolygon handling
Files:
flows.py,project.py_river_target_corridornow defaults to per-target bufferdistances derived from SWORD's own
widthcolumn(
buffer = width/2 × width_buffer_factor, default factor1.05)instead of one flat constant for every target.
An explicit
extraction_buffer_metersstill overrides this entirely._iter_geometry_piecesloops over each disconnectedcorridor piece and issues a separate download/query for each — both
for ICESat-2 (exact polygon) and Sentinel-3/6 (per-piece envelope).
_simplify_to_one_polygoncollapses a multipolygonreservoir into one convex-hull shape, so it's still queried as a
single target — fixed a genuine pre-existing gap in
_download_reservoirs_icesat2, which was silently dropping everypart of a multipolygon but the first.
math checked against real buffer-area calculations, not just
eyeballed.
7. Plotting fixes + river corridor shading
Files:
plotting.py,flows.pyplot_river_crossings: fixed hardcodedzoom=15→zoom="auto"(a river AOI can span far more area than a reservoir polygon). Added
per-target color-coding and reach midpoint markers.
plot_river_data: was reading directly from the raw, unfilteredHydrocron CSV, disconnected from the actual clean/merge pipeline.
Fixed to read the real merged output via a
get_merged_fncallback.plot_merging: References updated processing steps. Added the two neweroptional steps, fixed subplot-count/single-file edge cases, split
PLATFORM_COLORS(mission-level) fromSATELLITE_COLORS(instance-level, since the real
platformcolumn values are e.g."S3A"not"sentinel3").plot calls (
_has_enough_observations_to_plot).corridor_geometryparameter —plot_river_crossingscannow render the actual buffered extraction corridor (see section 6).
generate_rivers_summariescomputes this via the same
_river_target_corridorcall (sameresolved buffer parameters) actually used for extraction.
8. Misc bug fixes
Files:
project.py,rivers.yamlriver support — fixed to only fire when neither reservoirs nor
rivers is configured.
rivers.yamltemplate had SWOT's required config block commented outunder a misleading "advanced options" header, causing a real, silent
"SWORD downloads but nothing else does" failure mode. Updated the
template and added an explicit warning in
download_rivers()forthis exact case.
9. Rivers: SWORD-based extraction pipeline (core)
Files:
flows.py,project.pyThe largest single chunk of new work — extends the
reservoir-style clean/merge pipeline to river targets (SWORD nodes/reaches).
_river_target_corridor(base version — see section 6 for the laterwidth-based refinement),
_assign_points_to_river_targets(viagpd.sjoin_nearest, handling point-to-line matching natively)._download_rivers_icesat2/_download_rivers_sentinel: query over thecorridor rather than per-target.
_extract_rivers_*_observationsfunctions: split a singlewaterbody-wide download into the same per-target file structure
reservoirs already use, so everything downstream is shared rather
than duplicated. SWOT's version splits Hydrocron's per-waterbody CSV
the same way, attaching a placeholder geometry purely for file-format
consistency (nothing downstream reads it).
_clean_timeseries/_merge_timeseriesgeneralized (not duplicated)via a
target_typeparameter;_target_centroid/_get_target_idsgeneralize the old reservoir-only helpers the same way.
DEFAULT_RIVER_MERGING_OPTIONS, kept as a separate dict fromreservoirs' and explicitly flagged as not independently validated the
way the reservoir defaults were (this document
and PR_RUNBOOK.md both still describe this as unresolved).
project.py'screate_timeseries().architecture naturally supports multi-satellite river virtual
stations —
_merge_timeseriesloops over every configured missionfor a target with zero
target_type-specific restriction, exactly thesame code path as reservoirs.
feature_typebetween'nodes'/'reaches'isa pure config change, not a code change — every conditional
(SWORD file selection,
target_id_col, downstream plotting/extraction)branches on the config value rather than hardcoding either.
10. Sentinel-6 EarthData + credential check
Files:
sentinel/download.py,sentinel/__init__.py,sentinel/preprocess.py,flows.py,project.py(20Hz Ku-band) is available on PO.DAAC/EarthData.
query_earthdata/download_earthdatamirror SWOT's existingearthaccesspattern rather than porting a separate client.subset()extended to accept flat EarthData.ncfiles alongsideCREODIAS's zipped
.SEN6directories — the actual cropping logic iscompletely unchanged and shared. Verified end-to-end against a real
uploaded HR sample file (2614 rows, sane values) — the one piece of
this feature tested against real data rather than just reviewed.
_sentinel6_use_earthdata()/shared_download_sentinel_for_target()helper, branching on
mission_options["sentinel6"]["source"].Project._require_earthdata_credentials(). Confirmed viaearthaccess's own documentation that
earthaccess.login()(calledwith no explicit strategy, same as the existing SWOT code) falls
through environment variables →
.netrc→ interactive promptingif neither is present — hanging indefinitely in any non-interactive
run (a scheduled job, a CLI invocation) rather than failing clearly.
Unlike CREODIAS, which has an equivalent upfront check
(
_require_creodias_credentials), the EarthData path had none at all.Fixed with a matching upfront check, wired into both
_download_reservoirs_sentineland_download_rivers_sentinel.Verified the partial-credentials edge case (username set without
password) is correctly treated as missing, not silently accepted.
11. Spatial correction tools + stale progress file fix
Files:
basic_filters.py,timeseries.py,flows.pyapply_distance_penalty: inflates an observation's error based ondistance from a reference point, addressing well-sampled-but-
locationally-biased crossings that local ADM can't detect on its own.
Off by default.
fit_spatial_correction_model/apply_spatial_correction: agenuine height correction fit from a dense source (e.g. ICESat-2),
auto-detecting the most stable spatial axis via per-day slope
consistency across independent day-fits (not a single pooled
regression — confirmed empirically this matters). Off by default.
Persisted per-target, refit only on explicit
recalibrate=True.reservoir's data during review: the distance penalty recovers a
biased Kalman estimate from 299.94m to 299.98m against a true 300.0m;
the spatial correction's underlying slope was shown to be a real,
reproducible pattern (5 of 6 independent day-fits agree in sign, the
two best-supported — highest point count, highest R² — agree closely
with each other).
distance_penalty.csv/spatial_correction.csvare only written if that feature is activefor the current run, but the
merged_progressdirectory was nevercleared beforehand (
general.ifnotmakedirsonly creates it ifmissing). So a run with the feature toggled off would leave a stale
file from whenever it was last on, looking exactly like current
output. Fixed by explicitly removing both conditional files at the
start of
merge(), before any of the current run's steps writeanything. Verified the fix removes only these two files, leaving the
unconditionally-rewritten step files untouched.
12. Reach slope correction (new, opt-in)
Files:
flows.pyNew feature addressing a real gap found during review: SWOT's own
slope/slope_ufields (from the RiverSP reach product,hydrocron_fields.reaches) were being requested and extracted, butsilently dropped before ever reaching the merge pipeline —
PRODUCT_TIMESERIES_KEYS["swot"]never referenced them, andTimeseries.concat()only keeps key-mapped columns._fit_reach_slope_correction: reads the raw per-targetswot.gpkgdirectly (bypassing the genericTimeseries/concat()machinery, which is exactly where
slopewas being lost), takes themedian of all available
slopevalues for that reach (verifiedrobust to an injected outlier in testing), persists it to
reach_slope_correction_model.jsonfollowing the same fit-once/cache/invalidate-on-exclusion-change pattern as the spatial correction
model.
_apply_reach_slope_correction: for every non-SWOT row, projectsits position onto the reach's centerline (in a local metric CRS) to
get its along-reach distance from the geometric midpoint, then
adjusts
heightbyslope × distance. SWOT's own rows are leftuntouched. Verified with synthetic data: a crossing near the reach's
start (far from the midpoint) receives a real, sensible correction
matching the expected slope×distance magnitude; a crossing already
near the midpoint receives essentially none; SWOT rows are completely
unaffected either way.
_merge_timeseriesasuse_reach_slope_correction(off bydefault), gated to only ever activate when
prj.rivers.target_id_col == "reach_id"— a no-op for node-modeprojects, since a ~200m node doesn't have the same along-target slope
concern a ~10km reach does.
explicitly called out in both this document and the PR description
recommended in PR_RUNBOOK.md:
midpoint-referenced — an evidence-based inference from the
RiverSP processing chain (reach WSE is an aggregate of ~50 roughly
evenly-spaced nodes, not a value evaluated at one specific point),
not a fact directly confirmed in SWOT's product documentation.
height - slope × distance) hasnot been empirically checked against real data — confirm it
reduces cross-mission scatter for a real reach, not increases it,
before relying on this in production.
reasoning for
DEFAULT_RIVER_MERGING_OPTIONS's distance-penalty/spatial-correction defaults claimed a river target's footprint is
"much smaller than a reservoir" — confirmed wrong for reaches
specifically (~10km typical length), so those tools may matter just
as much for reaches as for reservoirs. Comment corrected in section 6.
Known open items:
merging_optionsdefaults (section 9) still notindependently validated against real river data.
orbit_key/stability (section 4) never re-verifiedagainst real S6 data the way Sentinel-3's was.
explicitly unvalidated — flagged again here on purpose, since it's
the newest and least-tested addition.