Curve number hydrology with Earth Observation. cnkit implements the NRCS
curve number method as it is actually used when the inputs come from satellite
data: the runoff relations and the initial abstraction ratio conversions, the
NLCD land cover by hydrologic soil group lookup with its assumptions stated
rather than buried, storm event and baseflow separation, the Hawkins asymptotic
curve number and its competing estimators, and the antecedent moisture
conventions.
The argument the library is built around:
Earth Observation has moved the curve number from an unverifiable lookup to a measurable, spatially and temporally resolved, uncertainty-quantified index. In doing so it has made visible the fact that the remaining error is dominated by the 1954 equation, not by the inputs.
That is why the API returns disagreements instead of hiding them. Where a
quantity has more than one defensible definition, cnkit computes all of them
and hands you the spread: composite_runoff returns three runoff depths,
compare_estimators returns four curve numbers, compare_conventions returns
the antecedent classes that the 1972 rainfall rule and the satellite soil
moisture percentile assign to the same storm.
pip install cnkitThe core science needs only numpy, pandas and scipy. Network fetchers and watershed delineation need an HTTP client; Earth Engine support is a separate optional extra:
pip install "cnkit[data]"
pip install "cnkit[gee]"
# or install both optional feature sets
pip install "cnkit[all]"From a clone, for development:
git clone https://github.com/skp703/cnkit.git
cd cnkit
pip install -e ".[dev,docs]"Python 3.9 or newer.
>>> from cnkit import runoff, composite_runoff
>>> round(float(runoff(3.0, 75)), 4) # 3 inches of rain on CN 75
0.9608
>>> # The weighting trap from Module 3.
>>> # 60 percent impervious at CN 98, 40 percent woods at CN 55, 1 inch storm.
>>> qd, qcn, qs = composite_runoff(1.0, [98, 55], [0.6, 0.4])
>>> [round(x, 4) for x in (qd, qcn, qs)]
[0.4745, 0.0949, 0.0277]Those three numbers are all "the runoff from this watershed". The first computes runoff per subarea and then averages. The second averages the curve number and then computes runoff. The third averages the retention S and then computes runoff. They differ by a factor of five and seventeen.
The reason is that CN 55 has Ia = 1.64 inches, so under the distributed calculation the wooded 40 percent contributes exactly nothing to a 1 inch storm, while the lumped composite CN of 80.8 has Ia = 0.48 inches and quietly assumes the entire watershed is partly absorbing. The gap narrows as the storm grows, to roughly two-fold at 2 inches, which is why nobody notices the problem when they sanity-check their work against a 100-year event. Which of the three goes in the report is a real engineering decision, and almost nobody makes it consciously.
That example is a doctest. It runs in CI on every push.
| Module | What it does |
|---|---|
cnkit.core |
S, Q, initial abstraction, back-calculation of CN from observed P and Q, the lambda 0.05 against lambda 0.20 conversions, the ARC 1 and ARC 3 adjustments, the Williams slope adjustment, and both composite CN weighting conventions. |
cnkit.lookup |
NLCD Level II crossed with hydrologic soil group to CN, with the hydrologic condition assumption made explicit per class, plus the condition spread and area-weighted composites. |
cnkit.events |
Storm event separation from a rainfall series, baseflow separation by local minimum and by the Lyne and Hollick filter, and the rainfall runoff event table the asymptotic fit consumes. |
cnkit.asymptotic |
The Hawkins asymptotic CN, the standard, complacent and violent behaviour classification, and the four competing estimators that do not agree with each other. |
cnkit.antecedent |
The 1972 five-day antecedent rainfall AMC class set against the satellite soil moisture percentile, with a day-of-year climatology and a disagreement table. |
cnkit.data |
No-authentication fetchers for USGS NWIS, USDA Soil Data Access and EPA StreamCat, plus the workshop watershed registry. |
cnkit.delineate |
Watershed polygons from a pour point or identifier using USGS NLDI, USGS SS-Delineate or the opt-in global mghydro service. |
cnkit.gee |
Optional basin reductions over Annual NLCD, impervious surface and hydrologic soil groups; cnkit.gee.timeseries adds precipitation, soil moisture and terrain. |
cnkit.workflows |
The end-to-end curve-number trajectory, returned with its hydrologic-condition spread and provenance. |
cnkit.exceptions |
Typed failures and warnings shared by network, delineation and Earth Engine paths. |
- Rainfall, runoff and storage depths are in inches unless a function says otherwise. Drainage area arguments state their units in their names.
- Curve numbers are dimensionless.
S_from_CNdeliberately clips values to[1, 100]; composite calculations reject values outside(0, 100]because silently weighting an invalid CN would contaminate the entire result. - Initial abstraction ratio
lammust be a finite scalar between 0 and 1. - Missing numerical inputs propagate as
NaNwhere a numerical result can be returned honestly. Structurally invalid inputs raiseValueError. - Functions accept NumPy arrays where vector operation is meaningful. Event and climatology functions require pandas Series with a DatetimeIndex.
Install the published package in any notebook environment; cloning this repository is not required:
%pip install -q "cnkit[gee]"
import os
from getpass import getpass
from cnkit.gee import initialise
# Entered at runtime, so the project id is not saved in notebook source.
project = os.environ.get("CNKIT_EE_PROJECT") or getpass("Earth Engine project id: ")
initialise(project=project)The first session on a machine may open Google's normal Earth Engine
authentication flow. cnkit never bundles credentials, writes to Earth Engine
assets or records a project id. For CONUS work use soils="sda"; the only
other enabled choice is the experimental global soils="hihydrosoil" path.
The 800 m CSRL community raster passed its Virginia coverage and complete
reduction smoke tests on 6 August 2026, but the owner kept it disabled for 1.1:
its coarse resolution, missing embedded class metadata and community-asset
durability make it unsuitable as a public release option.
The runnable examples are in examples/. Notebook 1 needs only a
network connection. Notebooks 2 and 3 need the runtime project value above and
work in JupyterLab or Colab.
cnkit computes runoff volume. It does not compute peak discharge, time to
peak, time of concentration, or hydrographs. The curve number method is a
volume method. TR-55 chapter 4, unit hydrograph convolution and every routing
step downstream of that are out of scope, and pairing a volume from this
library with a peak from somewhere else is your decision to document.
Two range limits are worth stating before anyone runs a design with these numbers:
- CN below 30. The NRCS tables are not defined below CN 30 and the method is not reliable there. Very low curve numbers imply retention values so large that the initial abstraction alone exceeds most design storms, and the result is a runoff depth of zero that is an artefact of the equation rather than a hydrologic finding.
- Runoff below 0.5 inches. Below roughly half an inch of computed runoff the method's error is the same size as its answer. Back-calculated curve numbers from small events are the main source of the apparent scatter in event data, which is exactly why the asymptotic fit exists.
Neither limit is enforced by the code. The functions will happily return a number outside them. Checking that you are inside them is the analyst's job.
The suite has seven layers, and they fail for different reasons on purpose.
- Unit and validation tests on the public API. Pure hydrology, boundary validation, empty inputs, units and optional-dependency behavior.
- Offline service-contract tests. Mocked NWIS, Soil Data Access, StreamCat and delineation responses exercise parsing without a network.
- Offline Earth Engine contract tests. Injectable executors assert exact assets, bands, reducers, scales, decoding and provenance without a project.
- Regression tests pinned to the workshop data pack. Every number that
appears on a workshop slide is asserted against the data it was computed
from, so a slide claim and the code cannot silently diverge. These read
tests/data. - Notebook expectation tests. The values the Colab notebooks print for attendees are asserted here, so a change to the package that would alter what forty people see on their screens fails the build first.
- Data pack integrity tests. Row counts, column names, date ranges, units
and checksums for the 1.8 MB pack in
tests/data. If an input file is regenerated or truncated, this layer says so rather than letting layer 4 fail with a confusing numeric mismatch. - Opt-in live smoke tests. Public-service tests require
CNKIT_NETWORK=1; Earth Engine tests requireCNKIT_EE_PROJECT. Both skip in normal CI.
Run them:
pip install -e ".[test]"
pytest # every layer
pytest tests src/cnkit # every layer plus the package doctests
pytest tests/test_cnkit.py -q # layer 1 only, 65 tests
pytest -m "not slow" # skip the long fits and anything networked
pytest --cov=cnkit --cov-report=term-missing
flake8 src tests toolsFor the private Earth Engine smoke test, authenticate once and provide the project id only through the current shell environment:
earthengine authenticate
printf "Earth Engine project id: "
read -r CNKIT_EE_PROJECT
export CNKIT_EE_PROJECT
pytest tests/test_gee_live.py tests/test_workflows_live.py -v -s
unset CNKIT_EE_PROJECTDo not place the project id in a notebook cell, .env file, workflow or
committed test configuration. The live tests skip when it is absent.
Doctests are collected by default (--doctest-modules), so the quick start
above and the examples in the module docstrings are executable specifications,
not illustrations.
CI runs the whole matrix on Python 3.9 through 3.13 on Linux, plus macOS and Windows on 3.12, and separately verifies that the single-file notebook build is in sync:
python tools/flatten.py # regenerate dist/cnkit_flat.py
python tools/flatten.py --check # fails if the flat file has driftedtools/flatten.py concatenates the five science modules into one importable
file for Colab attendees who have no pip install step. It is generated, never
edited by hand, and CI fails on drift.
If you use cnkit, cite the software and the workshop it was written for.
Machine-readable metadata is in CITATION.cff; GitHub renders
a formatted citation from it, and most reference managers import it directly.
Ramirez-Avila, J. J., and Kumar, S. (2026). cnkit: curve number hydrology with Earth Observation, version 1.1.0. Software accompanying the workshop "Modern Curve Number Hydrology: Fundamentals and Remote Sensing Applications", 2026 ASCE-EWRI Watershed Management Conference.
This is the library behind the workshop "Modern Curve Number Hydrology: Fundamentals and Remote Sensing Applications" at the 2026 ASCE-EWRI Watershed Management Conference, developed for the EWRI Curve Number Hydrology and Remote Sensing Task Committees. It is released for workshop use and for anyone who wants to reproduce the workshop's results. There is no warranty. Verify before you design anything.
Licensed under the MIT License. See LICENSE and
CHANGELOG.md.