WEPP Soil File Utilities
Parsing, transforming, and exporting WEPP soil input files
See also: AGENTS.md for coding conventions and soil file specification for WEPP format details.
Overview
The wepppy.wepp.soils.utils package provides utilities for working with WEPP soil input files (.sol). These tools enable parsing existing WEPP soil files, transforming parameters, migrating between file format versions, and composing multi-OFE (Overland Flow Element) soil definitions.
Key Capabilities:
- Parse WEPP soil files (versions 97.5, 2006.2, 7777, 7778, 9001-9005)
- Modify soil parameters (saturation, conductivity, erodibility)
- Migrate between WEPP file format versions
- Apply parameter replacements for scenario modeling
- Compose multi-OFE soil files from single-OFE components
- Export to WEPP
.solor BSON formats
Primary Users:
- NoDb controllers transforming soils for disturbed lands, BAER analysis, treatments
- Wildfire analysts applying burn severity adjustments
- Researchers modifying hydraulic parameters for sensitivity analysis
- WEPP model integrators reading soil properties from existing files
Package Organization:
wepppy/wepp/soils/utils/
├── __init__.py # Package exports
├── wepp_soil_util.py # WeppSoilUtil class (primary interface)
├── multi_ofe.py # SoilMultipleOfeSynth for multi-OFE composition
└── utils.py # Texture classification and lookup helpers
WeppSoilUtil Class
The primary interface for WEPP soil file manipulation. Loads a soil file into a structured dictionary, provides methods to transform parameters, and writes modified files back to disk.
Basic Usage
from wepppy.wepp.soils.utils import WeppSoilUtil
# Load a WEPP soil file
soil = WeppSoilUtil('/path/to/soil.sol')
# Access properties
print(f"Clay: {soil.clay}%, Sand: {soil.sand}%")
print(f"Texture: {soil.simple_texture}")
print(f"Soil depth: {soil.soil_depth} mm")
# Modify parameters
soil.modify_initial_sat(0.85) # Set 85% saturation
soil.modify_kslast(0.5) # Adjust restrictive layer conductivity
# Write modified soil
soil.write('/path/to/modified_soil.sol')
Constructor
WeppSoilUtil(
fn: str,
compute_erodibilities: bool = False,
compute_conductivity: bool = False
)
Parameters:
fn- Path to.solor.bsonfilecompute_erodibilities- Recalculate interrill/rill/shear from texture (first horizon)compute_conductivity- Recalculate hydraulic conductivity using WEPP formulas
Supported File Formats:
.sol- Native WEPP soil files (all versions).bson- Binary JSON serialization
Key Properties
Texture and Composition
soil.clay # Clay percentage (first horizon)
soil.sand # Sand percentage (first horizon)
soil.bd # Bulk density (g/cm³)
soil.rock # Rock fragment percentage
soil.simple_texture # Coarse classification: 'silt loam', 'loam', 'sand loam', 'clay loam'
soil.simple_texture_enum # Integer enum for texture (1-4)
Soil Profile
soil.soil_depth # Total soil depth (mm) from last horizon
soil.avke # Effective hydraulic conductivity (mm/h)
soil.datver # WEPP file format version (7778, 9002, etc.)
Modification Methods
Adjust Initial Saturation
soil.modify_initial_sat(initial_sat: float) -> None
Set the initial saturation level across all OFEs. Saturation is the fraction of pore space filled with water (0.0 - 1.0).
Example:
# Set to 75% saturation (typical field capacity)
soil.modify_initial_sat(0.75)
Adjust Restrictive Layer Conductivity
soil.modify_kslast(kslast: float, pars: Optional[Dict[str, Any]] = None) -> None
Update the hydraulic conductivity of the restrictive layer below the soil profile. Skips developed land use soils.
Parameters:
kslast- Conductivity in mm/hpars- Optional parameter dict for provenance tracking
Example:
# Reduce restrictive layer conductivity to simulate bedrock
soil.modify_kslast(0.1)
Clip Soil Depth
soil.clip_soil_depth(max_depth: float) -> None
Limit cumulative soil depth while preserving horizon ordering. Horizons exceeding max_depth are truncated or removed.
Example:
# Limit soil profile to 1 meter
soil.clip_soil_depth(1000.0) # mm
Version Migration Methods
Migrate to Version 7778
soil.to7778(hostname: str = '') -> WeppSoilUtil
Returns a new WeppSoilUtil instance migrated to version 7778 format (full parameterization with anisotropy). Missing parameters estimated via Rosetta pedotransfer functions.
Example:
# Migrate old soil file to modern format
soil_7778 = soil.to7778(hostname='analysis.server.edu')
soil_7778.write('/path/to/upgraded_soil.sol')
Estimation Applied:
bd(bulk density): Default 1.4 g/cm³ if missingfc(field capacity): Rosetta prediction from texturewp(wilting point): Rosetta prediction from textureksat: Rosetta prediction, converted cm/day → mm/hanisotropy: 10.0 for shallow layers (<50mm), 1.0 for deeper layers
Migrate to Disturbed Land Formats
soil.to9001(replacements, h0_min_depth=None, h0_max_om=None, hostname='') -> WeppSoilUtil
soil.to9002(replacements, h0_min_depth=None, h0_max_om=None, hostname='') -> WeppSoilUtil
soil.to9003(replacements, h0_min_depth=None, h0_max_om=None, hostname='') -> WeppSoilUtil
soil.to9005(replacements, h0_min_depth=None, h0_max_om=None, hostname='') -> WeppSoilUtil
Migrate to disturbed land formats (9001-9005) with burn severity and hydrophobicity adjustments.
Parameters:
replacements- Dict of parameter overrides (see below)h0_min_depth- Minimum depth for first horizon (mm)h0_max_om- Maximum organic matter for first horizon (filter if exceeded)hostname- Server name for provenance tracking
Replacement Parameters:
replacements = {
'ki': 5000000, # Interrill erodibility
'kr': 0.005, # Rill erodibility
'shcrit': 3.0, # Critical shear stress
'ksat': 10.0, # Saturated conductivity (mm/h)
'ksflag': 1, # Enable WEPP internal adjustments
'ksatadj': 1, # Enable keff adjustments
'ksatfac': 2.0, # Lower bound for keff (9001)
'ksatrec': 0.02, # Recovery rate (9001)
'lkeff': 1.0, # Lower limit on keff (9003, 9005)
'uksat': 50.0, # Upper limit on keff (9005)
'kslast': 0.01, # Restrictive layer conductivity
'luse': 'forest high sev', # Disturbed class
'stext': 'loam', # Simple texture
}
Multiplicative Replacements:
Use * prefix to multiply existing values:
replacements = {
'ksat': '*0.5', # Reduce conductivity by 50%
'kr': '*2.0', # Double rill erodibility
}
Example - High Severity Burn:
burn_replacements = {
'luse': 'forest high sev',
'stext': 'loam',
'ksatadj': 1,
'lkeff': 1.0, # Minimum keff = 1 mm/h
'ki': '*1.5', # Increase interrill erodibility
'kr': '*2.0', # Increase rill erodibility
}
burned_soil = soil.to9003(
replacements=burn_replacements,
h0_min_depth=100.0, # Ensure first horizon ≥ 100mm
hostname='fire-analysis.usda.gov'
)
burned_soil.write('/soils/burned_high_sev.sol')
Version Differences:
- 9001: Exponential keff recovery (
ksatfac,ksatrec) - 9002: Saxton & Rawls keff calculation with Rosetta van Genuchten parameters
- 9003: Burn severity codes, lower keff limit (
lkeff) - 9005: Revegetation modeling with upper keff limit (
uksat), texture enum
Internal Structure
The WeppSoilUtil.obj dictionary contains the parsed soil structure:
{
'header': [ # Comment lines from file
'WEPPcloud v.0.1.0',
'Build Date: 2025-10-23',
...
],
'datver': 7778.0, # File format version
'solcom': 'Any comments:', # User comment line
'ntemp': 1, # Number of OFEs
'ksflag': 1, # Conductivity adjustment flag
'ofes': [ # List of OFE definitions
{
'slid': 'Palouse', # Soil name
'texid': 'silt loam',
'nsl': 4, # Number of soil layers
'salb': 0.23, # Albedo
'sat': 0.75, # Initial saturation
'ki': 2940000, # Interrill erodibility
'kr': 0.0041, # Rill erodibility
'shcrit': 3.5, # Critical shear
'avke': 14.58, # Effective conductivity
'luse': None, # Disturbed class (9001+)
'stext': None, # Simple texture (9001+)
'ksatadj': 0, # keff adjustment flag (9001+)
'horizons': [ # Soil layers
{
'solthk': 210.0, # Depth (mm)
'bd': 1.32, # Bulk density
'ksat': 14.58, # Conductivity
'anisotropy': 10.0,
'fc': 0.289, # Field capacity
'wp': 0.132, # Wilting point
'sand': 8.5,
'clay': 19.0,
'orgmat': 3.5, # Organic matter
'cec': 18.6,
'rfg': 0.0, # Rock fragments
},
# ... more horizons
],
'res_lyr': { # Restrictive layer
'slflag': 1,
'ui_bdrkth': 10000.0,
'kslast': 0.00325,
},
},
],
'res_lyr': {...}, # Redundant restrictive layer reference
}
Serialization Methods
BSON Export
soil.dump_bson(dst: str) -> None
Export to binary JSON for efficient storage or transmission.
Example:
soil.dump_bson('/path/to/soil.bson')
WEPP File Output
soil.write(fn: str) -> None
# or use str() for content without writing
content = str(soil)
Write WEPP-formatted .sol file. The __str__() method generates the exact WEPP format.
Advanced Examples
Batch Transform for Wildfire Scenarios
from glob import glob
from wepppy.wepp.soils.utils import WeppSoilUtil
# Process all soils in watershed
for soil_fn in glob('/watershed/soils/*.sol'):
soil = WeppSoilUtil(soil_fn)
# Determine burn severity from filename
if 'high_sev' in soil_fn:
severity = 'high sev'
lkeff = 1.0 # Very low keff limit
else:
severity = 'low sev'
lkeff = 5.0
# Apply burn transformations
burned = soil.to9003(
replacements={
'luse': f'forest {severity}',
'stext': soil.simple_texture,
'ksatadj': 1,
'lkeff': lkeff,
'ki': '*1.2',
'kr': '*1.5',
},
h0_min_depth=100.0,
)
# Save to burn scenario directory
out_fn = soil_fn.replace('/soils/', '/burn_soils/')
burned.write(out_fn)
Sensitivity Analysis - Vary Conductivity
soil = WeppSoilUtil('/base/soil.sol')
for multiplier in [0.5, 0.75, 1.0, 1.25, 1.5]:
variant = soil.to7778()
# Multiply all horizon conductivities
for ofe in variant.obj['ofes']:
for horizon in ofe['horizons']:
horizon['ksat'] *= multiplier
# Write scenario file
variant.write(f'/scenarios/soil_ksat_{multiplier:.2f}.sol')
Migrate Legacy Soil Database
from pathlib import Path
legacy_dir = Path('/legacy/soils')
modern_dir = Path('/modern/soils')
modern_dir.mkdir(exist_ok=True)
for sol_file in legacy_dir.glob('*.sol'):
soil = WeppSoilUtil(str(sol_file))
# Skip if already 7778
if soil.datver == 7778.0:
continue
# Migrate to modern format
modern = soil.to7778(hostname='migration-server')
# Write with same filename
modern.write(str(modern_dir / sol_file.name))
print(f"Migrated {sol_file.name}: v{soil.datver} → v7778")
Extract Soil Properties for Reporting
import csv
from wepppy.wepp.soils.utils import WeppSoilUtil
soils = [WeppSoilUtil(fn) for fn in soil_files]
with open('soil_summary.csv', 'w', newline='') as f:
writer = csv.writer(f)
writer.writerow(['File', 'Texture', 'Clay%', 'Sand%', 'Depth(mm)', 'Albedo'])
for soil in soils:
writer.writerow([
soil.fn,
soil.simple_texture,
f'{soil.clay:.1f}',
f'{soil.sand:.1f}',
f'{soil.soil_depth:.0f}',
f'{soil.obj["ofes"][0]["salb"]:.3f}',
])
SoilMultipleOfeSynth
Utility class for composing multi-OFE (Overland Flow Element) WEPP soil files from multiple single-OFE soil definitions.
Purpose
WEPP models can have multiple OFEs representing different soil types along a hillslope profile. SoilMultipleOfeSynth combines individual soil files into a single multi-OFE file while ensuring format version consistency.
Usage
from wepppy.wepp.soils.utils import SoilMultipleOfeSynth
# Create synthesis object with stack of soil files
synthesis = SoilMultipleOfeSynth([
'/soils/top_slope.sol', # OFE 1
'/soils/mid_slope.sol', # OFE 2
'/soils/bottom_slope.sol', # OFE 3
])
# Write combined multi-OFE file
synthesis.write(
dst_fn='/combined/hillslope.sol',
ksflag=1 # Enable WEPP conductivity adjustments
)
Methods
SoilMultipleOfeSynth(stack: Optional[Iterable[str]] = None)
Constructor Parameters:
stack- Iterable of soil file paths (one per OFE), ordered top to bottom
Properties:
num_ofes- Number of OFEs in the stackstack_of_fns- Boolean indicating if all files exist
Methods:
write(dst_fn: str, ksflag: int = 0)- Write multi-OFE file
Example - Variable Soil Hillslope
from wepppy.wepp.soils.utils import SoilMultipleOfeSynth
# Define soils for each hillslope segment
stack = [
'/soils/ridge_top_sandy.sol', # Well-drained ridge
'/soils/backslope_loam.sol', # Transitional slope
'/soils/footslope_clayey.sol', # Poorly-drained toe
]
synthesis = SoilMultipleOfeSynth(stack)
print(f"Combining {synthesis.num_ofes} soils")
# Write multi-OFE file
synthesis.write('/watershed/hillslope_001.sol', ksflag=1)
Validation: All input soils must have the same WEPP file version. Mixing versions will raise an assertion error:
AssertionError: Soils must be of the same version ({7778.0, 2006.2})
Utility Functions
Texture Classification
from wepppy.wepp.soils.utils import simple_texture, simple_texture_enum
# Classify texture from clay and sand percentages
texture = simple_texture(clay=25.0, sand=35.0)
# Returns: 'loam'
# Get integer enum (1-4)
enum = simple_texture_enum(clay=25.0, sand=35.0)
# Returns: 2
Classification Logic:
clay loam(enum=1): High clay or low sandloam(enum=2): Balanced clay/sandsand loam(enum=3): High sandsilt loam(enum=4): Low clay+sand
Detailed Texture Classification
from wepppy.wepp.soils.utils import soil_texture
# USDA texture class
texture = soil_texture(clay=25.0, sand=35.0)
# Returns: 'loam'
Returns full USDA soil texture classification (sand, loamy sand, sandy loam, loam, silt loam, silt, sandy clay loam, clay loam, silty clay loam, sandy clay, silty clay, clay).
Soil Specialization Checks
from wepppy.wepp.soils.utils import soil_specialization, soil_is_water
# Check for special soil types
spec = soil_specialization(soil_name='Urban_1')
# Returns: 'urban'
spec = soil_specialization(soil_name='Water')
# Returns: 'water'
# Quick water check
is_water = soil_is_water(soil_name='water_7778_2')
# Returns: True
Modify Restrictive Layer Conductivity
from wepppy.wepp.soils.utils import modify_kslast
# Update kslast in existing file
modify_kslast(
fn='/path/to/soil.sol',
kslast=0.5,
datver=7778
)
Integration with NoDb Controllers
The WeppSoilUtil class is heavily used by NoDb controllers for soil transformations:
Soils Controller
from wepppy.nodb.core import Soils
from wepppy.wepp.soils.utils import WeppSoilUtil
soils = Soils.getInstance(wd)
# Inspect generated soil properties
for mukey, summary in soils.soils.items():
soil = WeppSoilUtil(summary.path)
print(f"{mukey}: {soil.simple_texture}, depth={soil.soil_depth}mm")
Disturbed Lands Controller
from wepppy.nodb.mods import Disturbed
disturbed = Disturbed.getInstance(wd)
# Controller automatically transforms base soils using WeppSoilUtil
# to apply burn severity adjustments via to9002() or to9003()
disturbed.build_disturbed_soils()
BAER Analysis
from wepppy.nodb.mods import Baer
baer = Baer.getInstance(wd)
# Applies SoilReplacements using WeppSoilUtil transformations
baer.apply_soil_replacements()
Developer Notes
Adding New WEPP File Versions
To support a new WEPP file version (e.g., 9006):
-
Update
_parse_sol()to recognize the version:elif solwpv == 9006: ksatadj, luse, new_param, stext, lkeff = line line = shlex.split(lines[i]) i += 1 -
Add migration method:
def to9006(self, replacements, ...) -> 'WeppSoilUtil': return self.to_over9000(replacements, ..., version=9006) -
Update
__str__()and__repr__()to output new format:elif datver == 9006.0: _new_param = ofe['new_param'] s.append(f"{_ksatadj}\t '{_luse}'\t {_new_param}\t ...") -
Update horizon line parsing if layer structure changes
-
Add tests in
tests/wepp/soils/utils/test_wepp_soil_util.py
Testing Soil Transformations
import pytest
from wepppy.wepp.soils.utils import WeppSoilUtil
def test_migration_to_7778():
# Load legacy soil
soil = WeppSoilUtil('tests/data/legacy_2006.sol')
assert soil.datver == 2006.2
# Migrate
modern = soil.to7778()
assert modern.datver == 7778.0
# Verify all horizons have required parameters
for ofe in modern.obj['ofes']:
for horizon in ofe['horizons']:
assert horizon['bd'] is not None
assert horizon['ksat'] is not None
assert horizon['anisotropy'] is not None
def test_parameter_replacement():
soil = WeppSoilUtil('tests/data/base.sol')
# Apply multiplicative replacement
modified = soil.to9002(replacements={'ksat': '*0.5'})
# Verify conductivity halved
original_ksat = soil.obj['ofes'][0]['horizons'][0]['ksat']
modified_ksat = modified.obj['ofes'][0]['horizons'][0]['ksat']
assert modified_ksat == pytest.approx(original_ksat * 0.5)
Provenance Tracking
All transformations append to the header list for full provenance:
soil = WeppSoilUtil('base.sol')
soil.modify_initial_sat(0.80)
soil.to9003(replacements={'luse': 'forest high sev'})
# Header now contains:
# wepppy.wepp.soils.utils.WeppSoilUtil::modify_initial_sat(initial_sat=0.8)
# wepppy.wepp.soils.utils.WeppSoilUtil::9003migration
# Build Date: 2025-10-23 14:32:15
# Source File: server:/base.sol
# Replacements
# --------------------------
# luse -> 'forest high sev'
These headers appear as comments in the output .sol file, providing audit trail for parameter modifications.
Performance Considerations
Parsing: Typical .sol file (4-8 horizons) parses in <10ms.
Migration: to7778() with Rosetta predictions takes ~50-100ms per soil due to pedotransfer calculations.
Batch Processing: For large soil databases (>1000 files), consider:
- Pre-parsing to BSON for faster repeated access
- Multiprocessing for parallel transformations
- Caching
WeppSoilUtilinstances if reused
Example - Parallel Migration:
from concurrent.futures import ProcessPoolExecutor
from wepppy.wepp.soils.utils import WeppSoilUtil
def migrate_soil(fn):
soil = WeppSoilUtil(fn)
modern = soil.to7778()
modern.write(fn.replace('/old/', '/new/'))
return fn
soil_files = glob('/old/soils/*.sol')
with ProcessPoolExecutor(max_workers=8) as executor:
results = executor.map(migrate_soil, soil_files)
for result in results:
print(f"Migrated {result}")
Known Limitations
Version Mixing
Multi-OFE files cannot mix WEPP versions. All component soils must be the same version before using SoilMultipleOfeSynth.
Workaround: Migrate all soils to target version first:
soils = [WeppSoilUtil(fn).to7778() for fn in soil_files]
for i, soil in enumerate(soils):
soil.write(f'/temp/soil_{i}.sol')
stack = [f'/temp/soil_{i}.sol' for i in range(len(soils))]
synthesis = SoilMultipleOfeSynth(stack)
synthesis.write('/final/multi_ofe.sol')
Rosetta Dependency
Version migration methods (to7778(), to9002(), etc.) require the rosetta package for pedotransfer functions. Ensure it's installed:
pip install rosetta-soil
Parameter Override Logic
The _replace_parameter() helper treats None, empty strings, and "none" (case-insensitive) as "do not replace." This prevents accidental nullification but may surprise users expecting explicit None to clear values.
Organic Matter Filtering
The h0_max_om parameter in migration methods filters out the first horizon if organic matter exceeds the threshold. This is irreversible - the horizon is removed from the output. Use cautiously.
Further Reading
WEPP Documentation
- Soil File Specification - Complete WEPP format specification
- WEPP Soil Parameters - Parameter descriptions and version history
- WEPP User Summary - Official WEPP model documentation
Related Modules
- wepppy.soils.ssurgo - SSURGO data acquisition and WEPP file generation
- wepppy.wepp.soils.horizon_mixin - Erodibility and conductivity calculations
- wepppy.wepp.soils.soilsdb - Pre-built WEPP soil library
- wepppy.nodb.core.soils - Soils NoDb controller
Scientific References
- 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.
- Schaap, M.G., Leij, F.J., van Genuchten, M.Th. (2001). "ROSETTA: a computer program for estimating soil hydraulic parameters with hierarchical pedotransfer functions." Journal of Hydrology, 251(3-4), 163-176.
Credits
Development:
- Roger Lew (rogerlew@gmail.com) - Primary developer
- University of Idaho - Institutional support
Funding:
- NSF Idaho EPSCoR Program (Award IIA-1301792)
- National Science Foundation
License: BSD-3-Clause (see license.txt)