Skip to content

Repository files navigation

cnkit

CI PyPI Python License: MIT

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.

Install

pip install cnkit

The 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.

Quick start

>>> 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.

API overview

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.

Units and input validation

  • 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_CN deliberately 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 lam must be a finite scalar between 0 and 1.
  • Missing numerical inputs propagate as NaN where a numerical result can be returned honestly. Structurally invalid inputs raise ValueError.
  • Functions accept NumPy arrays where vector operation is meaningful. Event and climatology functions require pandas Series with a DatetimeIndex.

Earth Engine in Jupyter or Colab

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.

What this library will not do

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.

Testing

The suite has seven layers, and they fail for different reasons on purpose.

  1. Unit and validation tests on the public API. Pure hydrology, boundary validation, empty inputs, units and optional-dependency behavior.
  2. Offline service-contract tests. Mocked NWIS, Soil Data Access, StreamCat and delineation responses exercise parsing without a network.
  3. Offline Earth Engine contract tests. Injectable executors assert exact assets, bands, reducers, scales, decoding and provenance without a project.
  4. 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.
  5. 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.
  6. 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.
  7. Opt-in live smoke tests. Public-service tests require CNKIT_NETWORK=1; Earth Engine tests require CNKIT_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 tools

For 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_PROJECT

Do 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 drifted

tools/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.

Citation

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.

Provenance

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.

About

Curve number hydrology with Earth Observation: NRCS runoff, NLCD/HSG lookup, event separation, asymptotic CN, and antecedent moisture.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages