Skip to content

Watershed model (SWAT+)

Daily water, sediment and nutrient balances of a watershed: the core of the SWAT+ model (USDA-ARS and Texas A&M AgriLife, version 62.0.1), ported to C++. For the part of SWAT+ it covers, the port gives SWAT+'s own results, value for value: weather (measured series and the weather generator), the land phase of each hydrologic response unit (snow, interception, soil temperature, potential and actual evapotranspiration, curve number runoff, MUSLE erosion, percolation, lateral flow, residue and nutrient cycling with the static carbon model, plant growth, scheduled operations and decision tables), routing units, 1-D aquifers and SWAT-DEG channels with variable storage or Muskingum routing, and the tables SWAT+ prints (by day, month, year and average annual). They live in the ls.hydro.swat module.

A model is a SwatProject: the files of a SWAT+ TxtInOut folder, read with load_txtinout, or made from the landscape. watershed divides a DEM with land use and soil maps into subbasins, channels and HRUs as QSWAT+ does, from_tables writes the project files as the SWAT+ Editor does (the same files), from_rasters does both. run simulates the project; hru_map and channel_map put the results back on the map. A project made here also runs in SWAT+ itself (SwatProject.write).

The erosion of MUSLE uses SWAT+ 62's cover factor C by default: the cover of the residue times the cover of the growing biomass, in which a tall canopy hardly protects the soil, so a forest's C (0.5-0.9 under little litter) can exceed a pasture's. run(..., c_factor="plant") takes the earlier SWAT+ method instead, the plants' minimum C (forest 0.001); only sediment and what travels with it change.

What the port does not simulate is refused with a clear error when a project uses it (reservoirs, wetlands, pesticides, salts, constituents, Green-Ampt infiltration, the CENTURY carbon model, water allocation...).

import landschaft as ls

project = ls.hydro.swat.load_txtinout("my_model/TxtInOut")
result = ls.hydro.swat.run(project, tables={"hru_ls": "aa", "channel_sd": "day"})
result.table("hru_ls", "aa")["sedyld"]          # t/ha per year of each HRU
result.table("channel_sd", "day")["flo_out"]    # m3/s leaving each channel

wx = ls.hydro.swat.climate(stations, pcp=pcp, tmax=tmax, tmin=tmin,
                           monthly={"pcp_hhr": 15.0, "slr_ave": 14.0, "dew_ave": 5.0, "wnd_ave": 3.0})
project = ls.hydro.swat.from_rasters(dem, landuse, soils, landuse_lookup=lu_codes, soil_lookup=soil_codes,
                                     soil_table=usersoil, climate=wx, stream_threshold=2.0)
erosion = ls.hydro.swat.hru_map(project, "sedyld")    # t/ha per year, on the DEM's grid

Projects

load_txtinout

load_txtinout(path) -> SwatProject

Read a SWAT+ TxtInOut folder, or a zip file of one (the outputs of earlier runs in it are left out).

Parameters:

Name Type Description Default
path str or path

The folder (with file.cio) or a .zip of its files.

required

Returns:

Type Description
SwatProject

Examples:

The Hubbard Brook W2 example of SWAT+ (SWATPlus-datasets, LGPL-2.1): 10 HRUs, a routing unit, 2 aquifers and a channel, 2000-2002 (its parameters.bsn was written for an earlier SWAT+: the peak rate factor is set to the 484 of SWAT+ 62):

>>> import landschaft as ls
>>> project = ls.hydro.swat.load_txtinout(ls.example_data("swat_hubbard_brook"))
>>> project
SwatProject(79 files)
>>> project.text("time.sim").splitlines()[2].split()
['0', '2000', '0', '2002', '0']

SwatProject dataclass

SwatProject(files: dict = dict(), watershed: object = None)

The input files of a SWAT+ model (the TxtInOut folder), by name.

Made by load_txtinout(), from_tables() or from_rasters(); run() simulates it and write() writes the folder for SWAT+ itself. The files are SWAT+ 62.0.1's formats; names are matched without regard to case, as on Windows.

Attributes:

Name Type Description
files dict

{file name: contents} (bytes, LF line ends).

Examples:

>>> import landschaft as ls
>>> project = ls.hydro.swat.load_txtinout(ls.example_data("swat_hubbard_brook"))
>>> "plants.plt" in project, project.text("time.sim").splitlines()[1].split()
(True, ['day_start', 'yrc_start', 'day_end', 'yrc_end', 'step'])
>>> copy = project.copy()
>>> copy.set_text("time.sim", project.text("time.sim").replace("2002", "2001"))
>>> import tempfile
>>> folder = copy.write(tempfile.mkdtemp())   # a TxtInOut folder SWAT+ itself can run
>>> sorted(p.name for p in folder.iterdir())[:4]
['HQ.tmp', 'STA1.tmp', 'STA14.tmp', 'STA17.tmp']

text

text(name: str) -> str

The contents of a file.

set_text

set_text(name: str, text: str) -> None

Replace (or add) a file.

print_file

print_file() -> str

The name of the print settings file (file.cio's simulation line, else print.prt).

set_print

set_print(tables=None, *, nyskip: int | None = None) -> None

Choose what SWAT+ prints (its print.prt): the years it skips before printing and, for each table, the steps printed.

Parameters:

Name Type Description Default
tables dict or list

{table: steps}, with steps from "day", "month", "year", "aa" (average annual), "all" or [] (none); a list of tables means all steps. Tables: hru_wb, hru_nb, hru_ls, hru_pw, basin_wb, basin_nb, basin_ls, basin_pw, channel_sd, aquifer (and the other rows of print.prt by their SWAT+ names).

None
nyskip int

Years simulated before printing (spin-up).

None

Examples:

>>> import landschaft as ls
>>> project = ls.hydro.swat.load_txtinout(ls.example_data("swat_hubbard_brook"))
>>> project.set_print({"hru_ls": ["year", "aa"], "hru_nb": []}, nyskip=1)
>>> [ln.split() for ln in project.text("print.prt").splitlines() if ln.startswith(("hru_ls", "hru_nb"))]
[['hru_nb', 'n', 'n', 'n', 'n'], ['hru_ls', 'n', 'n', 'y', 'y']]

write

write(path) -> Path

Write the files into a folder (made if needed), for SWAT+ to run.

Simulation

run

run(
    project: SwatProject,
    *,
    tables=None,
    nyskip: int | None = None,
    c_factor: str = "swatplus",
) -> SwatResult

Simulate a SWAT+ project, as SWAT+ 62.0.1 does.

Parameters:

Name Type Description Default
project SwatProject

The model (load_txtinout()).

required
tables dict or list

What to print, overriding the project's print.prt for this run (see set_print()), e.g. {"hru_wb": ["month", "year"], "channel_sd": "day"}.

None
nyskip int

Years simulated before printing, overriding print.prt.

None
c_factor ('swatplus', 'plant')

The cover and management factor C of MUSLE. "swatplus" (default), SWAT+ 62's: the cover of the residue times the cover of the growing biomass (from APEX), in which a tall canopy hardly protects the soil, so that a forest's C depends on its litter (0.5-0.9 under 1 t/ha of residue). "plant": SWAT+'s earlier method, which 62 keeps in its code but never uses (codes.bsn c_fact is ignored): C from the plants' minimum usle_c (plants.plt), raised towards 0.8 as cover falls (forest 0.001). Only sediment and what travels with it change; with "plant" the results are SWAT+'s with that method (checked against SWAT+ run so).

"swatplus"

Returns:

Type Description
SwatResult

Examples:

>>> import landschaft as ls
>>> project = ls.hydro.swat.load_txtinout(ls.example_data("swat_hubbard_brook"))
>>> result = ls.hydro.swat.run(project, tables={"hru_wb": "aa", "channel_sd": ["day", "aa"]}, nyskip=0)
>>> wb = result.table("hru_wb", "aa")              # average annual water balance of each HRU
>>> wb[["precip", "et", "surq_gen", "latq", "perc"]].astype(float).round(1).head(3)  # mm per year
                precip     et  surq_gen  latq   perc
date       hru
2002-12-31 1    1243.8  314.2     444.1   1.3  448.8
           2    1243.8  314.2     440.0  10.9  443.4
           3    1243.8  314.2     436.9  18.0  439.5
>>> q = result.table("channel_sd", "day")["flo_out"]  # m3/s leaving the channel
>>> round(float(q.mean()), 3), result.units["channel_sd"]["flo_out"]
(0.005, 'm^3/s')

SwatResult dataclass

SwatResult(
    weather: DataFrame,
    tables: dict = dict(),
    units: dict = dict(),
    warnings: list = list(),
)

Bases: Saveable

What run() simulated.

Attributes:

Name Type Description
weather DataFrame

The daily weather of each station (index date, station): precipitation (mm), maximum, minimum and mean temperature (deg C), solar radiation and its maximum (MJ/m2), relative humidity (fraction), dew point (deg C), wind speed (m/s), Hargreaves potential ET (mm), heat units, day length (h) and the fraction of the day's rain in its wettest half hour, measured or generated.

tables dict

What SWAT+ printed (its print.prt, see set_print()): {table: {step: DataFrame}}, tables hru_wb, hru_nb, hru_ls, hru_pw (water, nutrient, losses and plant/weather balances of each HRU), basin_wb... (the same for the basin), channel_sd (SWAT-DEG channels: water body, storage, inflow and outflow, water temperature) and aquifer; steps day, month, year, aa (average annual). Columns as SWAT+ names them in its output files (units in units); index date (the day SWAT+ printed the row: the day, the last day of the month or year, the end of the simulation) and the object (hru, channel, aquifer: SWAT+'s numbers), the basin tables by date only. Monthly and yearly values are sums of the daily ones, or means for states (soil water, CN, LAI, biomass...), as SWAT+ prints them.

units dict

{table: {column: unit}}.

warnings list

What SWAT+ would write to diagnostics.out (gages not found, ...).

Examples:

>>> import landschaft as ls
>>> project = ls.hydro.swat.load_txtinout(ls.example_data("swat_hubbard_brook"))
>>> result = ls.hydro.swat.run(project, tables={"basin_wb": ["month", "year"]}, nyskip=1)
>>> result
SwatResult(1096 days, tables: hru_wb[day/month/year/aa], hru_nb[day/month/year/aa], hru_ls[day/month/year/aa], hru_pw[day/month/year/aa], basin_wb[month/year], basin_nb[day/month/year/aa], basin_ls[day/month/year/aa], basin_pw[day/month/year/aa], channel_sd[day/month/year/aa], aquifer[day/month/year/aa])
>>> result.table("basin_wb", "year")[["precip", "et", "surq_gen", "perc"]].astype(float).round(1)
            precip     et  surq_gen   perc
date                                      
2001-12-31  1004.1  128.3     356.0  536.7
2002-12-31  1319.2  126.5     532.1  594.6
>>> result.units["basin_wb"]["et"]
'mm'

table

table(name: str, step: str = 'year') -> DataFrame

One printed table: result.table("hru_wb", "month").

From the landscape

watershed

watershed(
    dem,
    landuse,
    soils,
    *,
    landuse_lookup,
    soil_lookup,
    soil_table,
    stream_threshold: float | None = None,
    outlets=None,
    snap: float | None = None,
    slope_bands=(),
    hru_method: str = "dominant_hru",
    thresholds=(0.0, 0.0, 0.0),
    min_area: float = 0.0,
    area_in_ha: bool = True,
    exempt=(),
    n_threads: int | None = None,
) -> SwatWatershed

Divide a watershed into subbasins, channels and HRUs, as QSWAT+ does.

The DEM is filled (fill()), drains along D8 directions, and every cell whose drainage area reaches stream_threshold is a stream. Each stream link (stream_links(), split at the outlets) is a channel with its subbasin (watersheds()) and one landscape unit (QSWAT+ without landscapes); each subbasin has a shallow aquifer, each watershed outlet a deep one. HRUs are the combinations of land use, soil and slope band in a landscape unit, selected by hru_method. Channel width and depth come from the drainage area (QSWAT+'s 1.29 A^0.6 and 0.13 A^0.4, A in km2).

Parameters:

Name Type Description Default
dem

The elevations (m), in a projected CRS (or longitude / latitude).

required
landuse

Land use and soil maps (integer codes), resampled onto the DEM's cells (nearest neighbour) when their grids differ.

required
soils

Land use and soil maps (integer codes), resampled onto the DEM's cells (nearest neighbour) when their grids differ.

required
landuse_lookup

Land use code -> SWAT+ plant or urban name (plants.plt / urban.urb, e.g. {1: "FRSD", 2: "PAST"}), as a dict or a two-column table. Codes with the same name make one land use; missing cells take QSWAT+'s default land use (code 0 if listed, else the first listed).

required
soil_lookup

Soil code -> soil name (SNAM of the soil table), likewise.

required
soil_table

The soils: a usersoil table of ArcSWAT / QSWAT+ or one row per layer in SWAT+'s soils.sol columns (see the module notes); only the soils used are kept.

required
stream_threshold float | None

The drainage area of a stream, in km2 (default: 1 % of the DEM's area, as QSWAT+ proposes).

None
outlets

Watershed outlets (points; snap_outlets() moves them onto the streams within snap map units). Without them every stream that leaves the map (or ends in a pit) is an outlet.

None
slope_bands

Slope limits in percent between the bands, e.g. (5, 15) (default: one band, 0-9999).

()
hru_method ('dominant_hru', 'dominant', 'filter', 'area')

QSWAT+'s choices: one HRU per landscape unit with the largest combination ("dominant_hru", QSWAT+'s default) or with the dominant land use, soil and slope ("dominant"); all combinations above thresholds (land use, soil, slope, in percent; "filter"); or above min_area (ha, or percent with area_in_ha=False; "area"). Removed HRUs give their area to the others.

"dominant_hru"
exempt

Land use names never removed by the filters.

()

Returns:

Type Description
SwatWatershed

Examples:

>>> import landschaft as ls
>>> ex = ls.hydro.swat.example_landscape()

A 1 km2 stream threshold, one HRU per subbasin (QSWAT+'s default):

>>> w = ls.hydro.swat.watershed(ex["dem"], ex["landuse"], ex["soils"], landuse_lookup=ex["landuse_lookup"],
...                             soil_lookup=ex["soil_lookup"], soil_table=ex["soil_table"], stream_threshold=1.0)
>>> w
SwatWatershed(33 subbasins, 33 HRUs, 8075.9 ha)
>>> w.tables["gis_hrus"][["lsu", "landuse", "soil", "slope", "arsub"]].round(1).head(3)
   lsu landuse         soil  slope  arsub
0   10    FRSD  UPLAND_LOAM   31.9  228.5
1   20    FRSD  UPLAND_LOAM   47.9   38.4
2   30    FRSD  UPLAND_LOAM   23.0  116.9

Every combination above 10 % of land use, soil and slope (bands 0-15-35-9999 %):

>>> many = ls.hydro.swat.watershed(ex["dem"], ex["landuse"], ex["soils"], landuse_lookup=ex["landuse_lookup"],
...                                soil_lookup=ex["soil_lookup"], soil_table=ex["soil_table"], stream_threshold=1.0,
...                                hru_method="filter", thresholds=(10, 10, 10), slope_bands=(15, 35))
>>> len(many.tables["gis_hrus"])
128

SwatWatershed dataclass

SwatWatershed(
    tables: dict,
    subbasins: DataArray,
    hrus: DataArray,
    channels: DataArray,
    notes: list = list(),
)

A watershed divided as QSWAT+ divides it, ready for the SWAT+ Editor's import.

Made by watershed(); from_tables() turns it into a SWAT+ project.

Attributes:

Name Type Description
tables dict

QSWAT+'s project tables, as DataFrames with its columns: gis_subbasins, gis_channels, gis_lsus, gis_hrus, gis_aquifers, gis_deep_aquifers, gis_points, gis_routing, soils_sol and soils_sol_layer.

subbasins DataArray

The subbasin (= channel, SWAT+ numbering) of each cell, 0 outside the watershed, NaN where the DEM is missing.

hrus DataArray

The HRU of each cell (gis_hrus id), NaN for cells whose HRU was removed by the HRU selection (their area went to the HRUs kept) and outside.

channels DataArray

The channel of each stream cell, 0 elsewhere.

Examples:

>>> import landschaft as ls
>>> ex = ls.hydro.swat.example_landscape()
>>> w = ls.hydro.swat.watershed(ex["dem"], ex["landuse"], ex["soils"], landuse_lookup=ex["landuse_lookup"],
...                             soil_lookup=ex["soil_lookup"], soil_table=ex["soil_table"], stream_threshold=1.0)
>>> w.subbasins.shape, int(w.subbasins.max())
((371, 273), 33)
>>> sorted(w.tables)[:4]
['gis_aquifers', 'gis_channels', 'gis_deep_aquifers', 'gis_hrus']

from_tables

from_tables(
    watershed,
    climate: SwatClimate,
    *,
    start=None,
    end=None,
    codes: dict | None = None,
    name: str = "landschaft",
) -> SwatProject

A SWAT+ project from QSWAT+'s tables and the weather, as the SWAT+ Editor makes it.

Parameters:

Name Type Description Default
watershed SwatWatershed or dict

The subbasins, channels, landscape units, HRUs, aquifers, routing and soils: a SwatWatershed (watershed()) or QSWAT+'s project tables as DataFrames (gis_subbasins, gis_channels, gis_lsus, gis_hrus, gis_aquifers, gis_deep_aquifers, gis_routing, soils_sol, soils_sol_layer; load_qswat_project() reads them from a QSWAT+ project database).

required
climate SwatClimate

The weather (climate()).

required
start

The simulated period (default: the days all weather files cover, as the editor sets it).

None
end

The simulated period (default: the days all weather files cover, as the editor sets it).

None
codes dict

Columns of codes.bsn to change (e.g. {"pet": 2} for Hargreaves, {"rte_cha": 1} for Muskingum).

None
name str

The project name (object.cnt).

'landschaft'

Returns:

Type Description
SwatProject

Examples:

>>> import landschaft as ls
>>> ex = ls.hydro.swat.example_landscape()
>>> w = ls.hydro.swat.watershed(ex["dem"], ex["landuse"], ex["soils"], landuse_lookup=ex["landuse_lookup"],
...                             soil_lookup=ex["soil_lookup"], soil_table=ex["soil_table"], stream_threshold=1.0)
>>> wx = ls.hydro.swat.climate(ex["stations"], pcp=ex["pcp"], tmax=ex["tmax"], tmin=ex["tmin"], monthly=ex["monthly"])
>>> project = ls.hydro.swat.from_tables(w, wx)
>>> project
SwatProject(62 files)
>>> project.text("time.sim").splitlines()[2].split()
['0', '2001', '0', '2004', '0']
>>> print(project.text("hru-data.hru").splitlines()[2])
       1  hru01                    topohru01             hyd01       UPLAND_LOAM          frsd_lum        soilplant1              null           snow001              null

from_rasters

from_rasters(
    dem,
    landuse,
    soils,
    *,
    landuse_lookup,
    soil_lookup,
    soil_table,
    climate: SwatClimate,
    start=None,
    end=None,
    codes: dict | None = None,
    name: str = "landschaft",
    **options,
) -> SwatProject

A SWAT+ project from a DEM, land use and soil maps, tables and the weather.

watershed() divides the landscape as QSWAT+ does, from_tables() writes the project as the SWAT+ Editor does; the project runs in run() (or in SWAT+ itself, write()), and keeps the division (project.watershed) for hru_map() and channel_map().

Parameters:

Name Type Description Default
dem

As watershed().

required
landuse

As watershed().

required
soils

As watershed().

required
landuse_lookup

As watershed().

required
soil_lookup

As watershed().

required
soil_table

As watershed().

required
climate SwatClimate

The weather (climate()).

required
start

As from_tables().

None
end

As from_tables().

None
codes

As from_tables().

None
name

As from_tables().

None
**options

Passed to watershed(): stream_threshold, outlets, snap, slope_bands, hru_method, thresholds, min_area, area_in_ha, exempt.

{}

Returns:

Type Description
SwatProject

Examples:

>>> import landschaft as ls
>>> ex = ls.hydro.swat.example_landscape()
>>> wx = ls.hydro.swat.climate(ex["stations"], pcp=ex["pcp"], tmax=ex["tmax"], tmin=ex["tmin"], monthly=ex["monthly"])
>>> project = ls.hydro.swat.from_rasters(ex["dem"], ex["landuse"], ex["soils"], landuse_lookup=ex["landuse_lookup"],
...                                      soil_lookup=ex["soil_lookup"], soil_table=ex["soil_table"], climate=wx,
...                                      stream_threshold=1.0)
>>> project
SwatProject(62 files)
>>> project.watershed
SwatWatershed(33 subbasins, 33 HRUs, 8075.9 ha)
>>> result = ls.hydro.swat.run(project, tables={"basin_wb": "aa"})
>>> result.table("basin_wb", "aa")[["precip", "et", "surq_gen", "latq", "perc"]].astype(float).round(1)
            precip     et  surq_gen  latq   perc
date                                            
2004-12-31  1108.2  507.9     258.7  21.1  294.5

load_qswat_project

load_qswat_project(path) -> dict

The tables of a QSWAT+ project database, for from_tables().

Reads gis_subbasins, gis_channels, gis_lsus, gis_hrus, gis_aquifers, gis_deep_aquifers, gis_points, gis_routing, soils_sol and soils_sol_layer from the .sqlite QSWAT+ writes (the projects without landscapes, lakes, reservoirs or point sources).

Examples:

>>> import landschaft as ls
>>> ex = ls.hydro.swat.example_landscape()
>>> w = ls.hydro.swat.watershed(ex["dem"], ex["landuse"], ex["soils"], landuse_lookup=ex["landuse_lookup"],
...                             soil_lookup=ex["soil_lookup"], soil_table=ex["soil_table"], stream_threshold=1.0)
>>> import sqlite3, tempfile, pathlib
>>> db = pathlib.Path(tempfile.mkdtemp()) / "project.sqlite"
>>> with sqlite3.connect(db) as conn:
...     for name, table in w.tables.items():
...         _ = table.to_sql(name, conn, index=False)
... 
>>> tables = ls.hydro.swat.load_qswat_project(db)
>>> len(tables["gis_hrus"]), sorted(tables)[:3]
(33, ['gis_aquifers', 'gis_channels', 'gis_deep_aquifers'])

example_landscape

example_landscape() -> dict

The inputs of a small watershed model, from the package's example rasters.

The terrain is the example DEM "orelevation" (11 km x 8 km of the Oregon Cascades, 433-1390 m) projected to UTM zone 10 at 30 m; the land use comes from the NDVI "orforest" (forest, FRSD, where it exceeds 0.55, else pasture, PAST); the soils are two made-up soils by elevation (a group B loam above 900 m, a group C clay loam below); the weather is made up too: one station at the map's centre, daily rain and temperature for 2001-2004 from a seeded generator (wet winters, dry summers, about 1100 mm a year). Ready for watershed() / from_rasters() and climate().

Returns:

Type Description
dict

dem, landuse, soils (DataArrays on one grid), landuse_lookup, soil_lookup, soil_table (the arguments of watershed()), stations, pcp, tmax, tmin and monthly (the arguments of climate()).

Examples:

>>> import landschaft as ls
>>> ex = ls.hydro.swat.example_landscape()
>>> ex["dem"].shape, ex["dem"].rio.resolution()
((371, 273), (30.0, -30.0))
>>> sorted(ex["landuse_lookup"].values()), ex["pcp"].shape
(['FRSD', 'PAST'], (1461, 1))

Weather

climate

climate(
    stations,
    *,
    pcp,
    tmax,
    tmin,
    slr=None,
    hmd=None,
    wnd=None,
    wgn=None,
    monthly: dict | None = None,
) -> SwatClimate

The weather of a SWAT+ project from tables.

Parameters:

Name Type Description Default
stations DataFrame

One row per station (index: name), with lat, lon (degrees) and elev (m).

required
pcp

Daily DataFrames (index: dates; columns: station names): precipitation (mm), maximum and minimum temperature (deg C), solar radiation (MJ/m2), relative humidity (fraction) and wind speed (m/s). Missing days are NaN (-99 for SWAT+, which generates them); a variable SWAT+ does not get is generated.

required
tmax

Daily DataFrames (index: dates; columns: station names): precipitation (mm), maximum and minimum temperature (deg C), solar radiation (MJ/m2), relative humidity (fraction) and wind speed (m/s). Missing days are NaN (-99 for SWAT+, which generates them); a variable SWAT+ does not get is generated.

required
tmin

Daily DataFrames (index: dates; columns: station names): precipitation (mm), maximum and minimum temperature (deg C), solar radiation (MJ/m2), relative humidity (fraction) and wind speed (m/s). Missing days are NaN (-99 for SWAT+, which generates them); a variable SWAT+ does not get is generated.

required
slr

Daily DataFrames (index: dates; columns: station names): precipitation (mm), maximum and minimum temperature (deg C), solar radiation (MJ/m2), relative humidity (fraction) and wind speed (m/s). Missing days are NaN (-99 for SWAT+, which generates them); a variable SWAT+ does not get is generated.

required
hmd

Daily DataFrames (index: dates; columns: station names): precipitation (mm), maximum and minimum temperature (deg C), solar radiation (MJ/m2), relative humidity (fraction) and wind speed (m/s). Missing days are NaN (-99 for SWAT+, which generates them); a variable SWAT+ does not get is generated.

required
wnd

Daily DataFrames (index: dates; columns: station names): precipitation (mm), maximum and minimum temperature (deg C), solar radiation (MJ/m2), relative humidity (fraction) and wind speed (m/s). Missing days are NaN (-99 for SWAT+, which generates them); a variable SWAT+ does not get is generated.

required
wgn DataFrame

The weather generator statistics, as wgn_from_series() returns them (e.g. from SWAT+'s WGN database); computed from the series when omitted.

None
monthly dict | None

Passed to wgn_from_series() (pcp_hhr at least: SWAT's weather generator needs the largest half-hour rainfall of each month).

None

Returns:

Type Description
SwatClimate

Examples:

>>> import landschaft as ls
>>> ex = ls.hydro.swat.example_landscape()
>>> wx = ls.hydro.swat.climate(ex["stations"], pcp=ex["pcp"], tmax=ex["tmax"], tmin=ex["tmin"], monthly=ex["monthly"])
>>> wx
SwatClimate(1 stations, pcp, tmax, tmin)
>>> wx.period()
(Timestamp('2001-01-01 00:00:00'), Timestamp('2004-12-31 00:00:00'))
>>> wx.wgn.loc["valley", ["tmp_max_ave", "tmp_min_ave", "pcp_ave", "wet_dry", "pcp_days"]].round(2).head(3)
       tmp_max_ave  tmp_min_ave  pcp_ave  wet_dry  pcp_days
month                                                      
1             5.59        -4.83   151.68     0.78     21.75
2             7.40        -3.06   172.35     0.52     16.25
3            10.59        -0.04   106.60     0.39     15.25

SwatClimate dataclass

SwatClimate(stations: DataFrame, series: dict, wgn: DataFrame, notes: list = list())

The weather of a SWAT+ project (made by climate()).

Attributes:

Name Type Description
stations DataFrame

Index: the station names; lat, lon (degrees), elev (m).

series dict

{"pcp": ..., "tmax": ..., "tmin": ..., "slr": ..., "hmd": ..., "wnd": ...}: daily DataFrames (dates x stations; mm, deg C, MJ/m2, fraction, m/s; NaN for missing days), any of them absent.

wgn DataFrame

The weather generator: index (station, month 1-12), the WGN_COLUMNS, plus rain_yrs (years of rain record).

Examples:

>>> import landschaft as ls
>>> ex = ls.hydro.swat.example_landscape()
>>> wx = ls.hydro.swat.climate(ex["stations"], pcp=ex["pcp"], tmax=ex["tmax"], tmin=ex["tmin"], monthly=ex["monthly"])
>>> sorted(wx.series)
['pcp', 'tmax', 'tmin']
>>> wx.stations
              lat         lon      elev
valley  43.050142 -122.950156  899.0123

period

period() -> tuple[Timestamp, Timestamp]

The days every series covers (the SWAT+ Editor's default simulation period).

files

files() -> tuple[dict, list]

The weather files (name -> text) and, per file, (name, type, lat, lon).

wgn_from_series

wgn_from_series(
    pcp: DataFrame,
    tmax: DataFrame,
    tmin: DataFrame,
    *,
    slr=None,
    hmd=None,
    wnd=None,
    dewpoint=None,
    monthly: dict | None = None,
) -> DataFrame

SWAT's weather generator statistics (WXGEN) of each station, from daily series.

By calendar month over all years: the means and sample standard deviations of the daily maximum and minimum temperature; the mean monthly precipitation (pcp_ave: the month's total, averaged over the years with data), the standard deviation and skew coefficient of the daily precipitation (all days), the probabilities of a wet day after a dry and after a wet day, the mean number of wet days (precipitation above 0); the mean daily solar radiation, dew point and wind speed when the series are given.

Parameters:

Name Type Description Default
pcp DataFrame

Daily DataFrames (dates x stations): precipitation (mm), maximum and minimum temperature (deg C).

required
tmax DataFrame

Daily DataFrames (dates x stations): precipitation (mm), maximum and minimum temperature (deg C).

required
tmin DataFrame

Daily DataFrames (dates x stations): precipitation (mm), maximum and minimum temperature (deg C).

required
slr

Optional daily solar radiation (MJ/m2), wind speed (m/s) and dew point (deg C).

None
wnd

Optional daily solar radiation (MJ/m2), wind speed (m/s) and dew point (deg C).

None
dewpoint

Optional daily solar radiation (MJ/m2), wind speed (m/s) and dew point (deg C).

None
hmd

Daily relative humidity (fraction): with tmax and tmin it gives the dew point (Magnus' formula at the mean temperature) when dewpoint is not given.

None
monthly dict | None

Values not derived from daily data, by column (pcp_hhr: the largest half-hour rainfall of the month, mm; or slr_ave, dew_ave, wnd_ave): a scalar, 12 monthly values, or a DataFrame (month x station).

None

Returns:

Type Description
DataFrame

Index (station, month); the WGN_COLUMNS and rain_yrs.

Examples:

>>> import landschaft as ls
>>> ex = ls.hydro.swat.example_landscape()
>>> wgn = ls.hydro.swat.wgn_from_series(ex["pcp"], ex["tmax"], ex["tmin"], monthly=ex["monthly"])
>>> wgn.loc["valley", ["pcp_ave", "pcp_sd", "wet_dry", "wet_wet"]].round(3).head(3)
       pcp_ave  pcp_sd  wet_dry  wet_wet
month                                   
1      151.675   7.289    0.784    0.663
2      172.350   9.764    0.522    0.612
3      106.600   6.234    0.391    0.600

Results on the map

hru_map

hru_map(
    project, variable, *, how: str = "annual", result=None, c_factor: str = "swatplus"
) -> DataArray

An HRU result on the map: each cell takes the value of its HRU.

Parameters:

Name Type Description Default
project SwatProject

A project of from_rasters().

required
variable str or Series

A column of an HRU table (HRU_COLUMNS: e.g. "sedyld", the soil loss in t/ha; "wateryld", "surq_gen", "et" in mm; "bioms" in kg/ha), or values by HRU number (1..n).

required
how ('annual', 'mean', 'max', 'sum')

"annual": SWAT+'s average annual value (the yearly total of a flux, the mean of a state, after the years print.prt skips); or of the daily values: the mean, the largest, the sum.

"annual"
result SwatResult

A run of the project that printed the HRU table (aa for "annual", day otherwise), to map several variables of one run; by default the project runs.

None
c_factor ('swatplus', 'plant')

The USLE C factor of that run (see run()); ignored with result.

"swatplus"

Returns:

Type Description
DataArray

On the DEM's grid; NaN outside the watershed and on cells whose land use, soil and slope combination was folded into other HRUs.

Examples:

>>> import landschaft as ls
>>> ex = ls.hydro.swat.example_landscape()
>>> wx = ls.hydro.swat.climate(ex["stations"], pcp=ex["pcp"], tmax=ex["tmax"], tmin=ex["tmin"], monthly=ex["monthly"])
>>> project = ls.hydro.swat.from_rasters(ex["dem"], ex["landuse"], ex["soils"], landuse_lookup=ex["landuse_lookup"],
...                                      soil_lookup=ex["soil_lookup"], soil_table=ex["soil_table"], climate=wx,
...                                      stream_threshold=1.0)
>>> result = ls.hydro.swat.run(project, tables={"hru_ls": "aa", "hru_wb": "aa"}, c_factor="plant")
>>> loss = ls.hydro.swat.hru_map(project, "sedyld", result=result)   # t/ha per year, plants' minimum C
>>> loss.shape == ex["dem"].shape, round(float(loss.mean()), 3)
(True, 0.379)
>>> runoff = ls.hydro.swat.hru_map(project, "surq_gen", result=result)   # mm per year
>>> round(float(runoff.min()), 1), round(float(runoff.max()), 1)
(1.9, 445.5)

channel_map

channel_map(project, values=None, *, how: str = 'mean', result=None) -> DataArray

A channel result on the stream cells (by default the mean discharge, m3/s).

values: a column of the channel table (CHANNEL_COLUMNS, default "flo_out") or values by channel number; how and result as hru_map(). NaN off the streams.

Examples:

>>> import landschaft as ls
>>> ex = ls.hydro.swat.example_landscape()
>>> wx = ls.hydro.swat.climate(ex["stations"], pcp=ex["pcp"], tmax=ex["tmax"], tmin=ex["tmin"], monthly=ex["monthly"])
>>> project = ls.hydro.swat.from_rasters(ex["dem"], ex["landuse"], ex["soils"], landuse_lookup=ex["landuse_lookup"],
...                                      soil_lookup=ex["soil_lookup"], soil_table=ex["soil_table"], climate=wx,
...                                      stream_threshold=1.0)
>>> flow = ls.hydro.swat.channel_map(project)   # mean m3/s leaving each channel, on the stream cells
>>> round(float(flow.max()), 3)
0.609

reach_discharge

reach_discharge(project, *, result=None) -> DataFrame

The daily discharge leaving each channel (m3/s, channel_sd's flo_out): dates x channel number. result: a run that printed channel_sd by day.

Examples:

>>> import landschaft as ls
>>> ex = ls.hydro.swat.example_landscape()
>>> wx = ls.hydro.swat.climate(ex["stations"], pcp=ex["pcp"], tmax=ex["tmax"], tmin=ex["tmin"], monthly=ex["monthly"])
>>> project = ls.hydro.swat.from_rasters(ex["dem"], ex["landuse"], ex["soils"], landuse_lookup=ex["landuse_lookup"],
...                                      soil_lookup=ex["soil_lookup"], soil_table=ex["soil_table"], climate=wx,
...                                      stream_threshold=1.0)
>>> q = ls.hydro.swat.reach_discharge(project)   # days x channels, m3/s
>>> q.shape, round(float(q.max().max()), 2)
((1096, 33), 11.1)