Rasputin turns a digital elevation model (DEM) into a triangulated irregular network (TIN): a triangle mesh of the terrain that uses few triangles where the ground is smooth and many where it is rough, and never strays from the DEM by more than a height tolerance you choose.
The Bygdin catchment (305 km²) at a 10 m tolerance, each triangle coloured by
its CORINE land-cover class. Elevation © Kartverket (CC BY 4.0); land cover
© European Union, Copernicus Land Monitoring Service 2018, EEA. How it was made:
docs/benchmarks/2026-09-29/bygdin-landcover.md.
- DEM in, mesh out. One GeoTIFF, several, or a directory of tiles (Kartverket's DTM10, for example), stitched into one grid.
- DEMs from the network.
rasputin fetch glo30 --domain Xcopies the Copernicus GLO-30 tiles a mesh of that domain reads into a tile cache, sorasputin mesh --dem glo30then runs offline;anadem-v1(South America) likewise. - A domain. Mesh only the inside of a polygon, such as a catchment (GeoJSON or WKT, in any CRS that pyproj reads).
- Features as constraints. Lakes, land-cover borders, roads and rivers (GeoJSON, GeoPackage or GML) become edges of the mesh, so no triangle crosses them.
- Land-cover labels, cleaned up. With a CORINE class map, every triangle
gets the class of the polygon it lies in. First the borders are repaired
(
--features-repair), merged between neighbours of one class, snapped onto the domain outline where they nearly touch it, and simplified to--features-tolerancemetres with each class keeping its area. - Refinement to a tolerance. Triangles are split at the worst DEM point
until every triangle is within
--tolerancemetres of the DEM, then kept Delaunay, with a minimum-angle start so there are no slivers along the boundary. - A tolerance that varies.
--tolerance-near FILE Nwith--tolerance-ramp START ENDholds the mesh to N metres along named lines (a railway, a river) and ramps to the general tolerance with distance;--tolerance-slope N START ENDdoes the same on steep ground. - Catchments.
rasputin catchmentcomputes the catchment of a lake (a--seedpoint in it) or of a gauge on a mapped river (--rivers) from the DEM, and writes a GeoJSON polygon ready for--domain.rasputin fetch-stations nve-hrdfetches NVE's reference gauging stations with their catchments and rivers;rasputin station-catchmentsruns them all and compares.
Install first (INSTALL.md); the example needs the codecs
extra, because the committed DEM tile is LZW-compressed. The install is
bounds-checked by default; INSTALL.md's "Bounds checks" section says how to
build an unchecked copy for heavy runs. Every run uses every core unless
--threads N or the environment variable RASPUTIN_THREADS says otherwise
(0 means every core). From the repository root:
# A 50 km DTM10 tile, meshed inside a quarter disc of radius 30 km,
# to within 1 m of the DEM.
rasputin mesh \
--dem tests/fixtures/dem_archive/7908_3_10m_z33.tif \
--domain docs/benchmarks/2026-09-26/quarter.geojson \
--tolerance 1 \
--out quarter.vtk
# The same, with CORINE land cover as constraints and labels,
# at a 5 m tolerance.
rasputin mesh \
--dem tests/fixtures/dem_archive/7908_3_10m_z33.tif \
--domain docs/benchmarks/2026-09-26/quarter.geojson \
--features tests/fixtures/corine/clc2018_7908_3.gpkg --features-map corine \
--tolerance 5 \
--out quarter_landcover.vtk
# Natural colours for the land-cover classes, for ParaView.
rasputin palette corine --out corine.jsonThe first command writes about 430,000 triangles in under two seconds on a
laptop (Apple M1 Max). Add --stats - to print sizes, mesh quality and
timings, and --record run.json to write the run's record, for reproducing it.
rasputin --help lists the commands, and rasputin mesh --help explains
every option.
.vtk(legacy VTK, one file) is for ParaView. It holds the triangles, the constraint edges with their feature bits and, with a class map, the cell arrayland_cover_code. To colour by land cover, importcorine.jsonin ParaView's Colour Map Editor (Choose Preset, Import) and colour byland_cover_code..plyis for QGIS and other mesh tools. The surface goes to--out, and the constraint edges, if you want them, to a second file named by--out-edges.
Both formats are binary by default; --ascii writes text you can read with
head. Every mesh file records what it was made from, and stderr gets a
one-line summary of the run.
An AI agent (Claude Desktop, Codex, or any other client of the Model
Context Protocol, MCP) can drive rasputin through a small server built in.
Install the mcp extra (the dev extra includes it):
pip install "rasputin[mcp]"rasputin mcp then serves the commands over stdin and stdout. Point the
client at the rasputin of the environment you installed into, or at
python -m tin_engine mcp with that environment's python. Claude Desktop
takes this in claude_desktop_config.json (on macOS under
~/Library/Application Support/Claude/):
{"mcpServers": {"rasputin": {"command": "/path/to/venv/bin/rasputin", "args": ["mcp"]}}}Codex takes the same two fields in ~/.codex/config.toml:
[mcp_servers.rasputin]
command = "/path/to/venv/bin/rasputin"
args = ["mcp"]Those two fields are all a client needs. An env table is optional: with
env = { RASPUTIN_DATA = "/path/to/data" } the server reads the cache in
/path/to/data/cache, and a /path/to/data/sources.toml, if one exists, as
your sources file; without it, the cache is in ~/.cache/rasputin (or
$XDG_CACHE_HOME/rasputin).
The agent sees one tool per rasputin command (sources, mesh, catchment,
fetch, fetch_stations, ...; the names use underscores), each with the command's
options as its input schema, help text included, so an option added to the
CLI is a new tool argument with no further work. A call returns the exit code,
stdout and stderr, each output file as a file:// link and, for mesh, the
run record and the --stats report as data; the mesh itself never travels
through the agent. A refused command comes back as an error carrying the
CLI's own Error: words. During a long run the server reports every line the
command prints to stderr and a heartbeat every 5 s while it is silent; cancelling the
call kills the run. One call runs at a time unless --jobs N says more. One
caveat: a mesh cancelled while writing a .vtk or .ply can leave a
truncated file under the asked name, so rerun before reading it; a .gpkg
goes through a .part file and a rename, so it cannot.
sources is the one to call first, and the server tells the agent so: it lists
every elevation and land-cover source the machine knows, local and remote,
with its coverage as a longitude/latitude box, cell size in metres, CRS,
height unit, description and credit, read from the files' headers (with
bbox, the share of an area each covers). The agent passes the chosen
source's id as dem to mesh, catchment or station_catchments.
Data already on the machine is registered by name in your sources file,
~/.config/rasputin/sources.toml (or $XDG_CONFIG_HOME/rasputin/sources.toml;
a $RASPUTIN_DATA/sources.toml, if one exists, is read instead). One table per
dataset:
[source.my-terrain]
kind = "local"
path = "/data/terrain-10m" # a directory of GeoTIFF tiles, or one GeoTIFF
credit = "© The data's owner"
licence_note = "CC BY 4.0"
description = "Bare-ground terrain model, 10 m cells."A relative path is taken from the file's directory. The CRS, cell size and
coverage come from the GeoTIFFs themselves; z_unit = "metre" names the
height unit for files that do not state it. rasputin sources on the
command line shows the same listing, and says which sources file it read.
An installed Python package can add commands to rasputin, and they appear
in --help and as MCP tools like the built-in ones. It may also build a C++
extension module against rasputin's installed headers.
docs/plugins.md says what such a plugin declares, which
names it may import, and which headers it may include.
- A C++20 core (
include/terrain/,src/): exact geometric predicates, a constrained Delaunay triangulation, a noder that makes input lines meet cleanly, and the parallel refinement. The core never opens a file and knows nothing about coordinate systems. The only third-party C++ code is detria, vendored inlib/detria/. - A Python layer (
src_python/tin_engine/): reading GeoTIFFs (tifffile), vector files and CRS (shapely, pyproj), fetching remote data, configuration (Pydantic) and the CLI (Typer). The two meet through a pybind11 module,tin_engine._core.
project_structure.md describes the layout and the rules between the layers.
- INSTALL.md: installing, testing, and getting data.
- ROADMAP.md: what has been built and what comes next.
- docs/increments/: one design record per step of the work, with its measurements.
Bhattarai, B. C., Silantyeva, O., Teweldebrhan, A. T., Helset, S., Skavhaug, O., and Burkhart, J. F.: Impact of Catchment Discretization and Imputed Radiation on Model Response: A Case Study from Central Himalayan Catchment, Water, 12, 2020. https://doi.org/10.3390/w12092339
Silantyeva, O., Skavhaug, O., Bhattarai, B. C., Helset, S., Tallaksen, L. M., Nordaas, M., and Burkhart, J. F.: Shyft and Rasputin: a toolbox for hydrologic simulations on triangular irregular networks. https://doi.org/10.31223/X5CS95
MIT; see LICENSE. Third-party code and data credits are in NOTICE.md. Rasputin is developed by Expert Analytics.
