Skip to content

API reference

Classification API

pedotri.classify

pedotri.classify

classify(
    sand: float | int,
    clay: float | int,
    classification: str | Classification,
    *,
    locale: Locale | None = ...,
    detailed: Literal[False] = ...,
    units: str = ...,
) -> str | None
classify(
    sand: ArrayLike,
    clay: ArrayLike,
    classification: str | Classification,
    *,
    locale: Locale | None = ...,
    detailed: Literal[False] = ...,
    units: str = ...,
) -> list[str | None]
classify(
    sand: float | int,
    clay: float | int,
    classification: str | Classification,
    *,
    locale: Locale | None = ...,
    detailed: Literal[True],
    units: str = ...,
) -> ClassifyResult
classify(
    sand: ArrayLike,
    clay: ArrayLike,
    classification: str | Classification,
    *,
    locale: Locale | None = ...,
    detailed: Literal[True],
    units: str = ...,
) -> list[ClassifyResult]
classify(
    value: float | int,
    classification: str | Classification,
    *,
    locale: Locale | None = ...,
    detailed: Literal[False] = ...,
    units: str = ...,
) -> str | None
classify(
    value: ArrayLike,
    classification: str | Classification,
    *,
    locale: Locale | None = ...,
    detailed: Literal[False] = ...,
    units: str = ...,
) -> list[str | None]
classify(
    value: float | int,
    classification: str | Classification,
    *,
    locale: Locale | None = ...,
    detailed: Literal[True],
    units: str = ...,
) -> ClassifyResult
classify(
    value: ArrayLike,
    classification: str | Classification,
    *,
    locale: Locale | None = ...,
    detailed: Literal[True],
    units: str = ...,
) -> list[ClassifyResult]
classify(
    *,
    classification: str | Classification,
    locale: Locale | None = ...,
    detailed: bool = ...,
    units: str = ...,
    **axis_fractions: ScalarOrArrayLike,
) -> Any
classify(
    *args: Any,
    classification: str | Classification | None = ...,
    locale: Locale | None = ...,
    detailed: bool = ...,
    units: str = ...,
    method: str = ...,
    n_samples: int = ...,
    seed: Any = ...,
    **kwargs: Any,
) -> Any
classify(
    *args: Any, **kwargs: Any
) -> (
    str
    | None
    | list[str | None]
    | ClassifyResult
    | list[ClassifyResult]
)

Classify one or many soil texture points.

The number and meaning of fraction arguments depends on the chosen classification's :attr:~pedotri.schema.Classification.axes. The function accepts three calling styles:

Positional, axes-ordered::

pedotri.classify(70, 20, "USDA")  # sand=70, clay=20
pedotri.classify(10, "KACHINSKY")  # physical_clay=10

Keyword fractions with positional classification::

pedotri.classify(sand=70, clay=20, classification="USDA")

Mixed::

pedotri.classify(70, classification="USDA", clay=20)

The argument shapes are documented through the overload signatures above. In short:

  • fraction values — one positional argument per axis of the chosen classification, in axis order. Each value is either a scalar percentage in [0, 100] or an array-like of percentages. Fractions may also be passed by axis name as keyword arguments (sand=..., clay=..., physical_clay=...).
  • classification — last positional argument, or the classification= keyword. May be a classification key string (e.g. "USDA") or a :class:Classification instance.
  • locale — optional locale tag. When provided, class names are localized via the fallback chain (fr-FRfren); when omitted, results use the stable class keys.
  • detailed — when True, returns :class:ClassifyResult objects with name, group, parent, and signed distance-to-boundary rather than bare strings.
  • units — keyword. "%" (default), "g/kg", or "g/g". Applied uniformly to every fraction input. Use "g/kg" when your lab reports sand / clay / silt as g/kg (common in European soil chemistry), or "g/g" for the 0-1 mass-fraction convention.
  • _uncertainty — optional keyword per axis (e.g. sand_uncertainty, clay_uncertainty). Accepts a :class:~pedotri.uncertainty.Quantiles (recommended), a scalar or array σ, or — for backwards compatibility — a bare (Q0.05, Q0.95) 2-tuple (emits a DeprecationWarning). When any uncertainty kwarg is supplied, detailed is automatically promoted to True so probabilities and confidence can be returned, and the function returns a :class:ClassifyResult regardless of how detailed was passed.
  • method"distance" (default) or "monte_carlo". Only relevant when uncertainty kwargs are supplied. The distance method compares boundary distance to the input σ for a single confidence score; Monte Carlo draws n_samples realizations and returns a full probability distribution.
  • n_samples — int, default 1000. Number of Monte Carlo draws per input point.
  • seed — optional int or np.random.Generator. Seeds the Monte Carlo sampler for reproducibility.

Returns the matched class key (or localized name, or :class:ClassifyResult) for scalar input, or a list of those values for array-like input. None is returned for any point that lies outside every class polygon / interval.

Raises :class:InvalidInputError if fraction arrays have mismatched shapes or contain NaN / out-of-range values, or if the wrong number of fractions was provided. Raises :class:UnknownClassificationError if classification is a string that is not registered.

pedotri.ClassifyResult

pedotri.ClassifyResult dataclass

Detailed classification of a single point.

Attributes:

Name Type Description
key str | None

The matched class's stable key, or None if no class contains the point.

name str

The localized class name (or "" when key is None).

group str | None

Coarse grouping of the class (e.g. "fine") if defined.

parent str | None

Hierarchical parent class key if defined.

distance float

Signed Euclidean distance to the matched class boundary in the axis space. Positive means strictly inside; the larger the magnitude, the deeper the point sits inside its class. For 1-D classifications, this is the distance to the nearest interval endpoint. nan if the point is unclassified.

probabilities dict[str, float] | None

Mapping from class key to posterior probability when uncertainty information was supplied. None for the deterministic path. Keys with non-zero probability only; the unclassified mass (if any) is stored separately in :attr:unclassified_probability.

entropy float | None

Shannon entropy of :attr:probabilities in nats. None for the deterministic path. Maximum value is log(n_classes).

confidence float | None

Single scalar in [0, 1] summarizing how confident the classification is. For the distance method, this is the normal CDF of distance / σ_effective — points sitting many σ inside their class score near 1.0; points on a boundary score 0.5. For the Monte Carlo method, this is the modal-class probability. None for the deterministic path.

unclassified_probability float | None

Fraction of Monte Carlo samples that fell outside every class polygon. None for the distance and deterministic paths.

to_dict

to_dict() -> dict[str, Any]

Return a JSON-serializable dict representation.

nan is mapped to None so the result encodes cleanly with the standard library json module. Uncertainty-related fields are included only when populated, to keep the deterministic output unchanged.

pedotri.list_classifications

pedotri.list_classifications

list_classifications() -> list[str]

Return the sorted list of all registered classification keys.

pedotri.get_classification

pedotri.get_classification

get_classification(key: str) -> Classification

Return the registered classification for key (case-insensitive).

Built-ins and entry-point classifications are loaded lazily on first access. Raises :class:UnknownClassificationError if the key is not registered.

pedotri.register_classification

pedotri.register_classification

register_classification(
    source: str
    | Path
    | bytes
    | dict[str, Any]
    | Classification,
    *,
    overwrite: bool = False,
) -> Classification

Register a custom classification.

Parameters:

Name Type Description Default
source str | Path | bytes | dict[str, Any] | Classification

A TOML path, raw TOML bytes, a pre-parsed dict, or an already-constructed :class:Classification.

required
overwrite bool

If False (default), raise when a classification with the same key is already registered. Set to True to replace an existing entry — useful when iterating during development.

False

Returns:

Type Description
Classification

The (validated) :class:Classification instance.

pedotri.unregister_classification

pedotri.unregister_classification

unregister_classification(key: str) -> None

Remove a previously registered classification.

No-op if the key isn't registered. Useful in tests.

pedotri.load_classification

pedotri.load_classification

load_classification(
    source: str | Path | bytes | dict[str, Any],
) -> Classification

Load a :class:Classification from TOML.

Parameters:

Name Type Description Default
source str | Path | bytes | dict[str, Any]

Either a path to a .toml file, raw TOML bytes, or a pre-parsed dict (useful for tests and entry-point providers).

required

Returns:

Type Description
Classification

A validated :class:Classification instance.

Raises:

Type Description
ClassificationError

If the document is missing required fields or contains invalid data.

FileNotFoundError

If a path is given that does not exist.

Data model

pedotri.Classification

pedotri.Classification dataclass

A complete soil texture classification system.

Attributes:

Name Type Description
key str

Uppercase identifier (e.g. "USDA", "GEPPA").

axes tuple[str, ...]

Pair of particle-size fractions used as the x and y axes, e.g. ("sand", "clay"). The third fraction (silt) is implicit and equal to 100 - x - y.

classes tuple[TextureClass, ...]

Tuple of :class:TextureClass. Order is significant: the first class containing a point wins. This matches the convention used by most published soil texture triangles, where polygons are listed in canonical order and points on shared edges are deterministically attributed to the earlier-listed class.

names dict[Locale, str]

Localized names of the classification itself (for UI display).

default_locale Locale

Locale to use when none is explicitly requested.

reference str | None

Bibliographic reference for the polygon definitions.

url str | None

Optional canonical URL for the reference.

description str | None

Optional longer description of scope and intended use.

name

name(locale: Locale | None = None) -> str

Resolve the localized classification name.

class_by_key

class_by_key(key: str) -> TextureClass

Look up a class by its stable key.

Raises:

Type Description
KeyError

If key is not a class in this classification.

locales

locales() -> frozenset[Locale]

Return the union of all locales declared anywhere in this classification.

pedotri.TextureClass

pedotri.TextureClass dataclass

A single named texture class within a classification.

A class is either a polygon in 2-D space (when the parent classification has two axes, e.g. USDA's sand-clay plane) or an interval on a single axis (when the classification is 1-D, e.g. Kachinsky's physical clay fraction). Exactly one of :attr:vertices and :attr:interval must be set, matching the parent classification's :attr:Classification.axes length.

Treat instances as immutable after construction.

Attributes:

Name Type Description
key str

Stable machine identifier. Used as the return value of :func:pedotri.classify and as the lookup key for hierarchy and i18n.

vertices FloatArray | None

(N, 2) float array of polygon vertices in the (axis_x, axis_y) plane. Coordinates are percentages in [0, 100]. Closure is implicit. Set this for 2-D classes; leave :attr:interval as None.

interval tuple[float, float] | None

(low, high) tuple defining the half-open range [low, high) on the single axis. Set this for 1-D classes; leave :attr:vertices as None.

names dict[Locale, str]

Mapping of locale tag → localized display name. "en" is the recommended fallback.

group str | None

Optional coarse grouping (e.g. "fine", "medium", "coarse").

parent str | None

Optional parent class key for hierarchical classifications.

is_polygon property

is_polygon: bool

True if this class is a 2-D polygon, False if a 1-D interval.

name

name(locale: Locale = 'en') -> str

Resolve the localized class name with locale fallback.

Falls back through the locale chain fr-FR → fr → en and finally to self.key if no localized name is available.

Plotting

pedotri.plot.render_svg

pedotri.plot.render_svg

render_svg(
    classification: str | Classification,
    *,
    points: Sequence[Sequence[float]] | None = None,
    point_labels: Sequence[str] | None = None,
    locale: str | None = None,
    title: str | None = None,
    width: int = 720,
    show_legend: bool = True,
    show_grid: bool = True,
) -> str

Render a soil texture diagram as a standalone SVG string.

Parameters:

Name Type Description Default
classification str | Classification

Classification key (e.g. "USDA") or instance.

required
points Sequence[Sequence[float]] | None

Optional list of (sand, clay) pairs for 2-D classifications, or (value,) / scalars for 1-D. Drawn as overlay markers.

None
point_labels Sequence[str] | None

Optional labels for each marker.

None
locale str | None

Locale tag for class names in the legend; falls back to the classification's default locale.

None
title str | None

Optional title rendered above the diagram.

None
width int

SVG viewport width in pixels.

720
show_legend bool

Include a class legend to the right of the diagram.

True
show_grid bool

For 2-D, draw the 10 % interior grid lines.

True

Returns:

Type Description
str

SVG document as a string.

pedotri.plot.TextureDiagram

pedotri.plot.TextureDiagram

Renderable wrapper that displays inline in Jupyter notebooks.

Construct with the same arguments as :func:render_svg; the notebook will call :meth:_repr_svg_ to display the diagram.

render

render() -> str

Return the SVG string.

save

save(path: str | Path) -> None

Write the SVG document to path.

pedotri.plot.render_mpl

pedotri.plot.mpl.render_mpl

render_mpl(
    classification: str | Classification,
    *,
    points: Sequence[Sequence[float]] | None = None,
    point_labels: Sequence[str] | None = None,
    locale: str | None = None,
    title: str | None = None,
    show_legend: bool = True,
    show_grid: bool = True,
    figsize: tuple[float, float] = (8, 7),
) -> Figure

Render a texture diagram as a :class:matplotlib.figure.Figure.

Parameters:

Name Type Description Default
classification str | Classification

Classification key or instance.

required
points Sequence[Sequence[float]] | None

Optional (sand, clay) overlay points for 2-D classifications, or 1-tuples / scalars for 1-D.

None
point_labels Sequence[str] | None

Optional labels printed beside each marker.

None
locale str | None

Locale for class names in tooltips and legend.

None
title str | None

Optional figure title.

None
show_legend bool

Show class colour legend.

True
show_grid bool

For 2-D, draw 10 % interior gridlines.

True
figsize tuple[float, float]

(width, height) inches passed to plt.figure.

(8, 7)

Returns:

Type Description
Figure

Configured Figure ready for .savefig(...) or display.

Raises:

Type Description
ModuleNotFoundError

If matplotlib is not installed.

pedotri.plot.render_plotly

pedotri.plot.plotly_backend.render_plotly

render_plotly(
    classification: str | Classification,
    *,
    points: Sequence[Sequence[float]] | None = None,
    point_labels: Sequence[str] | None = None,
    locale: str | None = None,
    title: str | None = None,
    width: int = 720,
    height: int = 640,
) -> Any

Render a texture diagram as a :class:plotly.graph_objects.Figure.

Parameters:

Name Type Description Default
classification str | Classification

Classification key or instance.

required
points Sequence[Sequence[float]] | None

Optional (sand, clay) overlay points for 2-D, or 1-tuples / scalars for 1-D.

None
point_labels Sequence[str] | None

Optional labels shown alongside markers.

None
locale str | None

Locale for class names in tooltips and legend.

None
title str | None

Optional figure title.

None
width int

Pixel width.

720
height int

Pixel height.

640

Returns:

Type Description
Any

A plotly.graph_objects.Figure ready for .show() or

Any

.write_html(...).

Raises:

Type Description
ModuleNotFoundError

If plotly is not installed.

Pedotransfer functions

pedotri.ptf.saxton_rawls

pedotri.ptf.saxton_rawls

Saxton & Rawls (2006) pedotransfer function.

Estimates soil-water retention and saturated hydraulic conductivity from sand %, clay %, and organic matter %. Optionally adjusts for bulk-density compaction via a density factor and for gravel content.

The implementation follows the equations in:

Saxton, K.E. & Rawls, W.J. (2006). Soil water characteristic
estimates by texture and organic matter for hydrologic solutions.
Soil Science Society of America Journal, 70(5): 1569-1578.

Inputs are percentages (0-100) to match :func:pedotri.classify; the formulas use fractions internally.

SaxtonRawlsResult dataclass

Soil-water characteristics estimated by Saxton-Rawls (2006).

All water-content quantities are volumetric (cm³ water / cm³ bulk soil) — i.e. fractions in [0, 1].

Attributes:

Name Type Description
wilting_point float

Water content at 1500 kPa suction (θ_1500), the permanent wilting point.

field_capacity float

Water content at 33 kPa suction (θ_33), the conventional drained-equilibrium field capacity for agronomic soils.

saturation float

Water content at saturation (θ_s).

available_water float

Plant-available water = field_capacity - wilting_point.

saturated_conductivity float

Saturated hydraulic conductivity (K_s) in mm/h.

bulk_density float

Normal-density bulk density (g/cm³).

air_entry_tension float

Air-entry tension (bubbling pressure) ψ_e in kPa.

to_dict

to_dict() -> dict[str, Any]

Return a JSON-serializable dict representation.

saxton_rawls

saxton_rawls(
    sand: float | int,
    clay: float | int,
    organic_matter: float | int = ...,
    *,
    density_factor: float = ...,
    units: str = ...,
) -> SaxtonRawlsResult
saxton_rawls(
    sand: ArrayLike,
    clay: ArrayLike,
    organic_matter: ArrayLike | float | int = ...,
    *,
    density_factor: float = ...,
    units: str = ...,
) -> list[SaxtonRawlsResult]
saxton_rawls(
    sand: ScalarOrArrayLike,
    clay: ScalarOrArrayLike,
    organic_matter: ScalarOrArrayLike = 1.0,
    *,
    density_factor: float = 1.0,
    units: str = "%",
) -> SaxtonRawlsResult | list[SaxtonRawlsResult]

Compute soil hydraulic properties from texture and organic matter.

Parameters:

Name Type Description Default
sand ScalarOrArrayLike

Sand fraction. Default units: percent in [0, 100]. Scalar or array-like.

required
clay ScalarOrArrayLike

Clay fraction. Default units: percent in [0, 100]. Scalar or array-like.

required
organic_matter ScalarOrArrayLike

Organic-matter fraction (not organic carbon — see note below). Default units: percent in [0, 100]. Default value 1.0 % which is typical for agricultural mineral soils.

1.0
density_factor float

Compaction-adjustment multiplier on the regression's normal bulk density (Saxton & Rawls 2006, Eq. 6-7). Default 1.0 matches the original regression (no compaction). Values > 1.0 model compacted soils: saturation and field capacity are reduced via the paper's density correction. Wilting point is unaffected per the paper. Reasonable range is [0.9, 1.3].

1.0
units str

Units of the sand, clay, and organic_matter arguments. One of "%" (default), "g/kg" (lab reports from European labs often use this for organic matter), or "g/g" (0-1 mass fraction). density_factor is always a dimensionless multiplier regardless of units.

'%'

Returns:

Name Type Description
A SaxtonRawlsResult | list[SaxtonRawlsResult]

class:SaxtonRawlsResult for scalar inputs, or a list of

SaxtonRawlsResult | list[SaxtonRawlsResult]

results for array inputs.

Raises:

Type Description
InvalidInputError

If inputs are out of range or have mismatched shapes, or if units is unknown.

Note

Saxton-Rawls 2006 expects organic matter, but most lab reports give organic carbon. Convert with :func:pedotri.units.organic_carbon_to_organic_matter (Van Bemmelen factor 1.724) before calling.

pedotri.ptf.SaxtonRawlsResult

pedotri.ptf.SaxtonRawlsResult dataclass

Soil-water characteristics estimated by Saxton-Rawls (2006).

All water-content quantities are volumetric (cm³ water / cm³ bulk soil) — i.e. fractions in [0, 1].

Attributes:

Name Type Description
wilting_point float

Water content at 1500 kPa suction (θ_1500), the permanent wilting point.

field_capacity float

Water content at 33 kPa suction (θ_33), the conventional drained-equilibrium field capacity for agronomic soils.

saturation float

Water content at saturation (θ_s).

available_water float

Plant-available water = field_capacity - wilting_point.

saturated_conductivity float

Saturated hydraulic conductivity (K_s) in mm/h.

bulk_density float

Normal-density bulk density (g/cm³).

air_entry_tension float

Air-entry tension (bubbling pressure) ψ_e in kPa.

to_dict

to_dict() -> dict[str, Any]

Return a JSON-serializable dict representation.

pedotri.ptf.wosten

pedotri.ptf.wosten

Wösten et al. (1999) HYPRES pedotransfer function.

Estimates Mualem-van Genuchten water-retention and hydraulic-conductivity parameters from texture, organic matter, bulk density, and a topsoil/subsoil indicator. The implementation follows the continuous PTF of:

Wösten, J.H.M., Lilly, A., Nemes, A., Le Bas, C. (1999). Development
and use of a database of hydraulic properties of European soils.
Geoderma 90(3-4): 169-185.

The residual water content theta_r is fixed at 0.01, following the HYPRES convention.

WostenResult dataclass

Mualem-van Genuchten parameters from Wösten 1999.

The van Genuchten water-retention function is::

theta(h) = theta_r + (theta_s - theta_r) / [1 + (alpha*h)^n]^m
with m = 1 - 1/n

The Mualem unsaturated hydraulic-conductivity function is::

K(theta) = K_s * S_e^L * [1 - (1 - S_e^(1/m))^m]^2
with S_e = (theta - theta_r) / (theta_s - theta_r)

Attributes:

Name Type Description
theta_r float

Residual water content (m³/m³). Fixed at 0.01 in HYPRES.

theta_s float

Saturated water content (m³/m³).

alpha float

Van Genuchten alpha (1/cm).

n float

Van Genuchten n (dimensionless, > 1).

saturated_conductivity float

K_s in cm/day.

l float

Mualem pore-connectivity parameter, bounded by Wösten's logistic transform to (-10, 10). Note that the continuous PTF can yield negative L for many mineral soils; this is reproduced from the published regression and is not a sign error. If you need the strictly-positive Mualem L of typical theoretical interpretation, prefer the FAO-class class-averaged values in Wösten 1999 Table 4 or use a different PTF.

to_dict

to_dict() -> dict[str, Any]

Return a JSON-serializable dict representation.

wosten

wosten(
    sand: float | int,
    silt: float | int,
    clay: float | int,
    *,
    organic_matter: float | int = ...,
    bulk_density: float | int = ...,
    topsoil: bool = ...,
    units: str = ...,
) -> WostenResult
wosten(
    sand: ArrayLike,
    silt: ArrayLike,
    clay: ArrayLike,
    *,
    organic_matter: ArrayLike | float | int = ...,
    bulk_density: ArrayLike | float | int = ...,
    topsoil: bool = ...,
    units: str = ...,
) -> list[WostenResult]
wosten(
    sand: ScalarOrArrayLike,
    silt: ScalarOrArrayLike,
    clay: ScalarOrArrayLike,
    *,
    organic_matter: ScalarOrArrayLike = 1.0,
    bulk_density: ScalarOrArrayLike = 1.4,
    topsoil: bool = True,
    units: str = "%",
) -> WostenResult | list[WostenResult]

Estimate Mualem-van Genuchten parameters via Wösten 1999.

Parameters:

Name Type Description Default
sand ScalarOrArrayLike

Sand fraction. Default units: percent in [0, 100]. Used only for input validation; the PTF uses silt and clay explicitly.

required
silt ScalarOrArrayLike

Silt fraction. Default units: percent. Must be strictly positive — the PTF contains 1/silt and ln(silt) terms.

required
clay ScalarOrArrayLike

Clay fraction. Default units: percent. Must be strictly positive — the PTF contains 1/clay.

required
organic_matter ScalarOrArrayLike

Organic-matter fraction (not organic carbon; see :func:pedotri.units.organic_carbon_to_organic_matter). Default units: percent. Must be strictly positive. Default value 1.0 %.

1.0
bulk_density ScalarOrArrayLike

Dry bulk density (g/cm³). Default 1.4. Always in g/cm³ regardless of the units keyword.

1.4
topsoil bool

True if the sample is a topsoil horizon, False for subsoil. Affects alpha, n, and K_s.

True
units str

Units of sand, silt, clay, and organic_matter. One of "%" (default), "g/kg", or "g/g".

'%'

Returns:

Name Type Description
A WostenResult | list[WostenResult]

class:WostenResult (scalar inputs) or a list (arrays).

pedotri.ptf.WostenResult

pedotri.ptf.WostenResult dataclass

Mualem-van Genuchten parameters from Wösten 1999.

The van Genuchten water-retention function is::

theta(h) = theta_r + (theta_s - theta_r) / [1 + (alpha*h)^n]^m
with m = 1 - 1/n

The Mualem unsaturated hydraulic-conductivity function is::

K(theta) = K_s * S_e^L * [1 - (1 - S_e^(1/m))^m]^2
with S_e = (theta - theta_r) / (theta_s - theta_r)

Attributes:

Name Type Description
theta_r float

Residual water content (m³/m³). Fixed at 0.01 in HYPRES.

theta_s float

Saturated water content (m³/m³).

alpha float

Van Genuchten alpha (1/cm).

n float

Van Genuchten n (dimensionless, > 1).

saturated_conductivity float

K_s in cm/day.

l float

Mualem pore-connectivity parameter, bounded by Wösten's logistic transform to (-10, 10). Note that the continuous PTF can yield negative L for many mineral soils; this is reproduced from the published regression and is not a sign error. If you need the strictly-positive Mualem L of typical theoretical interpretation, prefer the FAO-class class-averaged values in Wösten 1999 Table 4 or use a different PTF.

to_dict

to_dict() -> dict[str, Any]

Return a JSON-serializable dict representation.

Particle-size conversions

pedotri.psd.convert

pedotri.psd.convert

convert(
    sand: ScalarOrArrayLike,
    silt: ScalarOrArrayLike,
    clay: ScalarOrArrayLike,
    *,
    source: str,
    target: str,
    units: str = "%",
) -> tuple[FloatArray, FloatArray, FloatArray]

Convert (sand, silt, clay) fractions between particle-size standards.

Parameters:

Name Type Description Default
sand ScalarOrArrayLike

Sand fraction under the source standard. Default units: percent in [0, 100]. See units.

required
silt ScalarOrArrayLike

Silt fraction under the source standard.

required
clay ScalarOrArrayLike

Clay fraction. Unchanged across all supported standards (the silt/clay boundary is universally 0.002 mm).

required
source str

Particle-size standard name of the input, one of :data:SAND_SILT_CUTOFF_MM's keys.

required
target str

Standard to convert to, same key set.

required
units str

Units of the three fraction arguments. One of "%" (default), "g/kg", or "g/g". The conversion is performed internally in percent and the output arrays are always returned in percent — call :func:pedotri.units.from_percent if you need to round- trip to the source units.

'%'

Returns:

Type Description
FloatArray

(sand', silt', clay) arrays in the target standard, in

FloatArray

percent. Outputs are always 1-D numpy arrays; pass

FloatArray

single-element arrays through float(...) if you need

tuple[FloatArray, FloatArray, FloatArray]

scalars.

Raises:

Type Description
InvalidInputError

For unknown standards, out-of-range percentages, or fractions whose sum deviates from 100 % by more than 0.5 percentage points.

Note

Conversion is not exactly reversible: convert(usda → isss → usda) generally differs from the input by a percent or two, reflecting the limitations of a single log-linear interior model.

pedotri.psd.interpolate_psd

pedotri.psd.interpolate_psd

interpolate_psd(
    sieve_sizes_mm: FloatArray,
    cumulative_pct_passing: FloatArray,
    target_size_mm: float,
) -> float

Interpolate a particle-size distribution onto a target sieve.

Given a measured PSD as (sieve_sizes, % passing each sieve), return the % of mass that would pass a sieve at target_size_mm, interpolating log-linearly between the nearest measured points.

This is the building block for arbitrary cutoff conversions when a richer PSD than the three-point sand/silt/clay split is available.

Parameters:

Name Type Description Default
sieve_sizes_mm FloatArray

Strictly-increasing sieve sizes in mm.

required
cumulative_pct_passing FloatArray

% of sample mass finer than each sieve, same length as sieve_sizes_mm. Must be non-decreasing with respect to sieve size.

required
target_size_mm float

Sieve size to interpolate onto, in mm.

required

Returns:

Type Description
float

% of sample mass finer than target_size_mm, in [0, 100].

Raises:

Type Description
InvalidInputError

For mismatched shapes, non-monotonic inputs, or a target outside the measured range.

pedotri.psd.SAND_SILT_CUTOFF_MM

pedotri.psd.SAND_SILT_CUTOFF_MM module-attribute

SAND_SILT_CUTOFF_MM: dict[str, float] = {
    "USDA": 0.05,
    "FAO": 0.05,
    "ISSS": 0.02,
    "INTERNATIONAL": 0.02,
    "KA5": 0.063,
}

Raster (GeoTIFF) classification

pedotri.raster.classify_array

pedotri.raster.classify_array

classify_array(
    sand: FloatArray | ndarray,
    clay: FloatArray | ndarray,
    *,
    classification: str | Classification,
    units: str = "%",
    mask: ndarray | None = None,
    sum_tolerance: float = 5.0,
) -> tuple[np.ndarray, list[str]]

Classify a raster of sand / clay fractions into integer codes.

Parameters:

Name Type Description Default
sand FloatArray | ndarray

Sand fraction raster (any shape). Default units are percent; pass units="g/kg" for SoilGrids-native rasters.

required
clay FloatArray | ndarray

Clay fraction raster, same shape as sand.

required
classification str | Classification

A registered 2-axis classification key ("USDA", "GEPPA", …) or a :class:Classification instance. Must use the (sand, clay) axis order — every built-in 2-D classification does.

required
units str

Units of the input rasters. "%" (default), "g/kg", or "g/g". Converted internally to percent before the point-in-polygon test.

'%'
mask ndarray | None

Optional boolean array, same shape as sand. False cells are written as :data:NODATA_CODE and skipped during classification. Useful for a water / land mask or externally-supplied nodata mask from rasterio.

None
sum_tolerance float

A pixel whose sand + clay exceeds 100 + sum_tolerance is treated as missing data (codes to :data:NODATA_CODE). The default of 5 percentage points accommodates SoilGrids' independently-modelled fractions, whose sum can deviate from 100 % by a few percent per pixel. Pass float("inf") to disable the check.

5.0

Returns:

Type Description
ndarray

A (codes, keys) tuple. codes is an int16 array of the

list[str]

same shape as sand: each entry is either an index into

tuple[ndarray, list[str]]

keys (matched class) or :data:NODATA_CODE. keys is

tuple[ndarray, list[str]]

the list of class keys in the order used by the codes — so

tuple[ndarray, list[str]]

keys[codes[i, j]] is the class key for pixel (i, j)

tuple[ndarray, list[str]]

when codes[i, j] != NODATA_CODE.

Raises:

Type Description
InvalidInputError

For shape mismatches or unknown units.

PedotriError

If the chosen classification has a single axis (1-D classifications are out of scope for raster classification — they would need a different input shape).

pedotri.raster.classify_geotiff

pedotri.raster.classify_geotiff

classify_geotiff(
    sand: Path | str,
    clay: Path | str,
    *,
    classification: str | Classification,
    units: str = "%",
    sum_tolerance: float = 5.0,
) -> tuple[np.ndarray, list[str], dict[str, Any]]

Read two GeoTIFFs, classify pixel-wise, return codes + profile.

Parameters:

Name Type Description Default
sand Path | str

Path to a single-band sand-fraction GeoTIFF.

required
clay Path | str

Path to a single-band clay-fraction GeoTIFF in the same CRS, resolution, and extent as sand. (Mismatches raise.)

required
classification str | Classification

Same as :func:classify_array.

required
units str

Units of both input rasters. Default "%". Pass "g/kg" for SoilGrids 2.0 tiles.

'%'
sum_tolerance float

Forwarded to :func:classify_array.

5.0

Returns:

Type Description
ndarray

(codes, keys, profile). profile is a rasterio profile

list[str]

dict with dtype="int16", nodata=NODATA_CODE, and the

dict[str, Any]

sand raster's CRS / transform / size, ready to pass to

tuple[ndarray, list[str], dict[str, Any]]

func:write_classified_geotiff.

Raises:

Type Description
ImportError

If the optional rasterio dependency is not installed (pip install pedotri[raster]).

InvalidInputError

If the two rasters disagree on shape, CRS, or transform.

pedotri.raster.write_classified_geotiff

pedotri.raster.write_classified_geotiff

write_classified_geotiff(
    path: Path | str,
    codes: ndarray,
    *,
    profile: dict[str, Any],
    keys: list[str],
    colormap: dict[int, tuple[int, int, int, int]]
    | None = None,
) -> None

Write a class-code raster as a GeoTIFF with class names + colors.

Output is a paletted uint8 GeoTIFF (the only dtype on which GDAL/QGIS persist a color table). The in-memory NODATA_CODE sentinel (-1, int16) is remapped to 255 on disk so the file still has a clean nodata value. Class keys are written both as band-level GDAL tags (class_<index>) and through rasterio's color-table API, so QGIS and gdalinfo automatically pick up the legend.

Parameters:

Name Type Description Default
path Path | str

Output path.

required
codes ndarray

int16 array from :func:classify_array or :func:classify_geotiff.

required
profile dict[str, Any]

rasterio profile to use for georeferencing. Typically the third element returned by :func:classify_geotiff.

required
keys list[str]

Class-key list returned alongside codes. Up to 255 classes are supported (every pedotri built-in fits).

required
colormap dict[int, tuple[int, int, int, int]] | None

Optional mapping {class_index: (R, G, B, A)} attached as the band's color table. When omitted, a qualitative palette is generated from matplotlib's tab20; falls back to a hand-built 12-tone palette if matplotlib isn't installed. The nodata entry (255) is forced transparent.

None

Raises:

Type Description
ImportError

If rasterio is not installed (pip install pedotri[raster]).

InvalidInputError

If len(keys) > 255.

pedotri.raster.smooth_codes

pedotri.raster.smooth_codes

smooth_codes(
    codes: ndarray,
    *,
    window: int = 3,
    iterations: int = 1,
    n_classes: int | None = None,
) -> np.ndarray

Apply a majority filter to a class-coded raster.

Replaces each pixel with the modal class index in a window x window neighborhood (so isolated mis-classifications get absorbed into their dominant surroundings — the standard salt-and-pepper cleanup for remote-sensing class maps).

Implementation: one-hot expand the codes, run a uniform-box filter on each class channel via :func:scipy.ndimage.uniform_filter, and take the per-pixel argmax across classes. All vectorized in C — orders of magnitude faster than a Python-callback mode filter on large rasters.

The filter preserves :data:NODATA_CODE (-1) pixels: any pixel that was nodata stays nodata; nodata pixels do not vote for their neighbors' new class.

Parameters:

Name Type Description Default
codes ndarray

int array of class codes (same convention as :func:classify_array). -1 is treated as nodata.

required
window int

Side length of the square neighborhood (odd, >= 3).

3
iterations int

How many smoothing passes to apply. Each pass re-applies the same filter; two or three passes converge quickly for typical noise patterns.

1
n_classes int | None

Number of distinct class indices to consider. Defaults to codes.max() + 1 (or 1 if the raster is empty / all-nodata). Pass explicitly when the raster might not contain every class (e.g. for an aligned ensemble where you want stable class indices across tiles).

None

Returns:

Type Description
ndarray

Smoothed code array, same shape and dtype as codes.

Raises:

Type Description
ImportError

If scipy is not installed (pip install pedotri[raster]).

InvalidInputError

For invalid window (must be odd, >= 3).

pedotri.raster.render_classified_png

pedotri.raster.render_classified_png

render_classified_png(
    path: Path | str,
    codes: ndarray,
    *,
    keys: list[str],
    title: str | None = None,
    extent: tuple[float, float, float, float] | None = None,
    xlabel: str | None = "longitude",
    ylabel: str | None = "latitude",
    figsize: tuple[float, float] = (8, 7),
    dpi: int = 150,
    cmap: str = "tab20",
) -> None

Render a class-coded raster as a PNG map with a side legend.

Parameters:

Name Type Description Default
path Path | str

Output PNG path.

required
codes ndarray

int array from :func:classify_array. -1 pixels are rendered transparent.

required
keys list[str]

Class-key list returned alongside codes. Each class index becomes a labeled tick on the colorbar.

required
title str | None

Optional figure title.

None
extent tuple[float, float, float, float] | None

(left, right, bottom, top) in the data CRS, used for axis ticks. If None, axes show pixel coordinates.

None
xlabel str | None

X-axis label. Defaults to "longitude" for WGS 84 maps; set to e.g. "easting (m)" for a projected CRS, or None to omit.

'longitude'
ylabel str | None

Y-axis label. Defaults to "latitude"; same convention as xlabel.

'latitude'
figsize tuple[float, float]

matplotlib figure size in inches.

(8, 7)
dpi int

Output DPI; 150 is good for ~1500-px web images, 300 for print.

150
cmap str

A qualitative matplotlib colormap. tab20 matches the default GeoTIFF color table.

'tab20'

Raises:

Type Description
ImportError

If matplotlib is not installed (pip install pedotri[matplotlib]).

pedotri.raster.classified_to_features

pedotri.raster.classified_to_features

classified_to_features(
    codes: ndarray,
    *,
    keys: list[str],
    transform: Any,
    simplify_tolerance: float | None = None,
) -> list[dict[str, Any]]

Polygonize a class-coded raster into per-class vector features.

Adjacent pixels sharing the same class index are merged into a single polygon (with holes where appropriate). nodata pixels (-1) are excluded.

Parameters:

Name Type Description Default
codes ndarray

int array of class codes.

required
keys list[str]

Class-key list returned alongside codes.

required
transform Any

rasterio Affine transform giving pixel→world coordinates. Typically profile["transform"].

required
simplify_tolerance float | None

If set, apply :meth:shapely.geometry.base.BaseGeometry.simplify to each polygon with this tolerance (units of the data CRS). Useful for big SoilGrids tiles where un-simplified polygons can have hundreds of thousands of vertices.

None

Returns:

Name Type Description
list[dict[str, Any]]

List of feature dicts ``{"geometry": shapely geom,

list[dict[str, Any]]

"properties": {"class_id": int, "class_key": str}}``. Pass

to list[dict[str, Any]]

func:write_features_shapefile to write to disk, or

list[dict[str, Any]]

feed directly into geopandas.GeoDataFrame.from_features.

Raises:

Type Description
ImportError

If rasterio or shapely is not installed (pip install 'pedotri[raster,vector]').

pedotri.raster.write_features_shapefile

pedotri.raster.write_features_shapefile

write_features_shapefile(
    features: list[dict[str, Any]],
    path: Path | str,
    *,
    crs: str = "EPSG:4326",
) -> None

Write features from :func:classified_to_features to a Shapefile.

Uses fiona directly so no GeoPandas dependency is required. For GeoPackage or GeoJSON output, change the file extension — fiona infers the driver from the path suffix.

Parameters:

Name Type Description Default
features list[dict[str, Any]]

List of feature dicts from :func:classified_to_features.

required
path Path | str

Output path. .shp writes ESRI Shapefile; .gpkg writes GeoPackage; .geojson writes GeoJSON.

required
crs str

Coordinate reference system as an authority string (e.g. "EPSG:4326"). Pass the CRS of the source raster profile["crs"].

'EPSG:4326'

Raises:

Type Description
ImportError

If shapely or fiona is not installed (pip install 'pedotri[vector]').

pedotri.raster.NODATA_CODE

pedotri.raster.NODATA_CODE module-attribute

NODATA_CODE: int = -1

Field-scale interpolation (pedotri.interp)

pedotri.interp.krige_samples

pedotri.interp.krige_samples

krige_samples(
    xs: Sequence[float] | ndarray,
    ys: Sequence[float] | ndarray,
    values: Sequence[float] | ndarray,
    *,
    bbox: tuple[float, float, float, float],
    resolution: float | tuple[float, float],
    variogram: VariogramModel = "spherical",
    n_lags: int = 6,
) -> tuple[np.ndarray, np.ndarray, dict[str, Any]]

Ordinary-krige a single property on a regular grid.

Parameters:

Name Type Description Default
xs Sequence[float] | ndarray

X coordinates of the N samples (1-D).

required
ys Sequence[float] | ndarray

Y coordinates of the N samples (1-D, same length as xs).

required
values Sequence[float] | ndarray

Property values at each sample. Length-N, same order as xs / ys.

required
bbox tuple[float, float, float, float]

Output extent (x_min, y_min, x_max, y_max). Should comfortably enclose all samples (the kriging prediction outside the convex hull of the samples is an extrapolation — pykrige will produce a value, but its kriging variance will be large there).

required
resolution float | tuple[float, float]

Output cell size. Either a single float (square cells) or a tuple (dx, dy). Units match xs / ys (typically degrees in WGS 84 or metres in a projected CRS).

required
variogram VariogramModel

Variogram model name passed to pykrige. "spherical" is the most common default for soil texture; "linear" is robust on tiny sample sets.

'spherical'
n_lags int

Number of lag bins used for empirical variogram estimation. pykrige's default is 6; raise for >50 samples, lower for very small N.

6

Returns:

Type Description
ndarray

A (grid, variance, profile) tuple. grid is the kriging

ndarray

prediction with shape (rows, cols) and dtype float64.

dict[str, Any]

variance is the kriging variance at each cell (same shape;

tuple[ndarray, ndarray, dict[str, Any]]

larger means lower confidence). profile is a rasterio-style

tuple[ndarray, ndarray, dict[str, Any]]

profile dict (transform, crs=None, width,

tuple[ndarray, ndarray, dict[str, Any]]

height, dtype="float64", count=1) suitable to feed

tuple[ndarray, ndarray, dict[str, Any]]

func:pedotri.raster.write_classified_geotiff after the grid

tuple[ndarray, ndarray, dict[str, Any]]

is classified. crs is left as None because the

tuple[ndarray, ndarray, dict[str, Any]]

interpolator doesn't know what CRS the coordinates are in —

tuple[ndarray, ndarray, dict[str, Any]]

set it on the returned profile before writing if you care.

Raises:

Type Description
ImportError

If pykrige is not installed (pip install 'pedotri[interp]').

InvalidInputError

For mismatched array lengths, fewer than three samples, or zero-area bbox.

pedotri.interp.krige_sand_clay

pedotri.interp.krige_sand_clay

krige_sand_clay(
    samples: Sequence[dict[str, float]] | ndarray,
    *,
    bbox: tuple[float, float, float, float],
    resolution: float | tuple[float, float],
    variogram: VariogramModel = "spherical",
    n_lags: int = 6,
    mask_polygon: Any | None = None,
    crs: str | None = None,
) -> tuple[np.ndarray, np.ndarray, dict[str, Any]]

Krige sand and clay independently from a single sample table.

This is the field-scale convenience function: given a list of in- situ samples (each with x, y, sand %, clay %), it kriges both fractions, optionally masks the result to a bounding polygon (so cells outside the field become nodata), and returns rasters suitable for :func:pedotri.raster.classify_array.

Parameters:

Name Type Description Default
samples Sequence[dict[str, float]] | ndarray

Either a list of dicts [{"x": ..., "y": ..., "sand": ..., "clay": ...}, ...] (key names must be x, y, sand, clay), or a 2-D numpy array of shape (N, 4) with columns (x, y, sand, clay).

required
bbox tuple[float, float, float, float]

Output extent (x_min, y_min, x_max, y_max), same as :func:krige_samples.

required
resolution float | tuple[float, float]

Output cell size, same as :func:krige_samples.

required
variogram VariogramModel

Variogram model name, same as :func:krige_samples.

'spherical'
n_lags int

Number of lag bins, same as :func:krige_samples.

6
mask_polygon Any | None

Optional shapely Polygon / MultiPolygon defining the field boundary. Cells whose centers fall outside the polygon are set to nan in both output rasters. Pass None to skip masking. Polygon coordinates must be in the same CRS as the samples.

None
crs str | None

Optional CRS authority string (e.g. "EPSG:4326") to stamp on the returned profile.

None

Returns:

Type Description
ndarray

(sand_grid, clay_grid, profile). Hand directly to

ndarray

func:pedotri.raster.classify_array:

dict[str, Any]

.. code-block:: python

codes, keys = classify_array(sand_grid, clay_grid, classification="USDA")

Raises:

Type Description
ImportError

If pykrige (or shapely, when mask_polygon is given) is not installed.

InvalidInputError

For malformed inputs (see :func:krige_samples).

Benchmark utilities

pedotri.bench.run_soiltexture_benchmark

pedotri.bench.run_soiltexture_benchmark

run_soiltexture_benchmark(
    sizes: list[int] | None = None,
    *,
    classification: str = "USDA",
    seed: int = 0,
    repeat: int = 3,
    check_agreement: bool = True,
) -> BenchmarkReport

Time pedotri vs. soiltexture on equivalent inputs.

Parameters:

Name Type Description Default
sizes list[int] | None

Sample counts to benchmark, e.g. [1_000, 10_000, 100_000]. Defaults to [1_000, 10_000, 100_000].

None
classification str

Pedotri classification key. Must be one of USDA / FAO / ISSS / INTERNATIONAL (the set both libraries share).

'USDA'
seed int

Random seed for input generation.

0
repeat int

Each timing is the minimum of repeat runs.

3
check_agreement bool

When True, also run both libraries at the smallest size, normalize soiltexture's label format ("sandy clay loam""sandy_clay_loam"), and record the fraction of points on which they agree.

True

Returns:

Type Description
BenchmarkReport

class:BenchmarkReport. Empty rows for soiltexture if the

BenchmarkReport

package is not importable — the pedotri timings are still

BenchmarkReport

reported so a CI run without the optional dependency degrades

BenchmarkReport

gracefully.

pedotri.bench.BenchmarkReport

pedotri.bench.BenchmarkReport dataclass

Collected timings + a 100 %-agreement check on outputs.

format

format() -> str

Pretty-print the report as a fixed-width table for stdout / READMEs.

speedup_table

speedup_table() -> list[tuple[int, float]]

Return [(n_points, pedotri_speed / soiltexture_speed), ...].

pedotri.bench.BenchmarkRow

pedotri.bench.BenchmarkRow dataclass

Timing result for one (n_points, library) cell.

pedotri.bench.generate_inputs

pedotri.bench.generate_inputs

generate_inputs(
    n: int, *, seed: int = 0
) -> tuple[np.ndarray, np.ndarray]

Generate n valid (sand, clay) samples in [0, 100] with sand+clay ≤ 100.

The distribution is uniform over the triangular feasible region {(s, c) : s ≥ 0, c ≥ 0, s + c ≤ 100}. This guarantees no nonsense inputs (impossible silt) and reasonable class coverage.

Exceptions

pedotri.PedotriError

pedotri.PedotriError

Bases: Exception

Base class for all pedotri exceptions.

pedotri.ClassificationError

pedotri.ClassificationError

Bases: PedotriError

Raised when a classification definition is invalid.

pedotri.UnknownClassificationError

pedotri.UnknownClassificationError

Bases: PedotriError, KeyError

Raised when a requested classification key is not registered.

pedotri.InvalidInputError

pedotri.InvalidInputError

Bases: PedotriError, ValueError

Raised when input arrays are malformed (shape mismatch, NaNs, out of range, etc.).