SSURGO tools

cycles.ssurgo

Ssurgo(path, state, *, lat_lon=None, boundary=None)

Load SSURGO lookup tables and extract soil profiles for locations.

Initialize SSURGO lookup tables and optional spatial subset.

When a location (lat_lon) or boundary polygon is provided, the map-unit table is filtered to include only those map units that intersect the location or polygon; the map-units are also grouped by name and symbol, and the major map unit is selected for profile extraction.

Parameters:
  • path (str | Path) –

    Directory containing SSURGO geodatabase and lookup CSV files.

  • state (str) –

    State identifier used in SSURGO file naming.

  • lat_lon (LatLon | None, default: None ) –

    Optional latitude/longitude for point-based spatial filtering.

  • boundary (GeoDataFrame | None, default: None ) –

    Optional boundary GeoDataFrame for polygon-based filtering.

Returns:
  • None

    None.

Raises:
  • ValueError

    If both lat_lon and boundary are provided.

Source code in cycles/ssurgo/ssurgo.py
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
def __init__(self, path: str | Path, state: str, *, lat_lon: LatLon | None=None, boundary: gpd.GeoDataFrame | None=None) -> None:
    """Initialize SSURGO lookup tables and optional spatial subset.

    When a location (`lat_lon`) or boundary polygon is provided, the map-unit table is filtered to include only
    those map units that intersect the location or polygon; the map-units are also grouped by name and symbol, and
    the major map unit is selected for profile extraction.

    Args:
        path: Directory containing SSURGO geodatabase and lookup CSV files.
        state: State identifier used in SSURGO file naming.
        lat_lon: Optional latitude/longitude for point-based spatial filtering.
        boundary: Optional boundary GeoDataFrame for polygon-based filtering.

    Returns:
        None.

    Raises:
        ValueError: If both ``lat_lon`` and ``boundary`` are provided.
    """
    _validate_geographic_input(lat_lon, boundary)

    self.state: str = state
    self._mapunits: gpd.GeoDataFrame | pd.DataFrame
    self.components: pd.DataFrame
    self.horizons: pd.DataFrame
    self.grouped_mapunits: MapUnitGeoDataFrame | None = None
    self.mukey: int | None = None
    self.slope: float | None = None
    self.hsg: str = ''

    path = Path(path)
    luts = _read_all_luts(path, state)
    self.components = luts['component']
    self.horizons = luts['horizon']

    if lat_lon is None and boundary is None:
        self._mapunits = luts['mapunit']
        return

    if lat_lon is not None:
        boundary = gpd.GeoDataFrame(
            {'name': ['point']},
            geometry=[Point(lat_lon[1], lat_lon[0])],
            crs='epsg:4326',
        )
    gdf = _read_mupolygon(path, state, boundary)
    self._mapunits = gdf.merge(luts['mapunit'], on='mukey', how='left')
    self.components = self.components[self.components['mukey'].isin(self._mapunits['mukey'].unique())]
    self.horizons = self.horizons[self.horizons['cokey'].isin(self.components['cokey'].unique())]

    self._group_map_units()
    self._select_major_mapunit()
    self._average_slope_hsg()

mapunits property

Return loaded map-unit table.

Returns:
  • MapUnitGeoDataFrame | DataFrame

    Map-unit table as GeoDataFrame/DataFrame, or None if not loaded.

muname property

Return map-unit name for the currently selected MUKEY.

Returns:
  • str

    Map-unit name string.

musym property

Return map-unit symbol for the currently selected MUKEY.

Returns:
  • str

    Map-unit symbol string.

non_soil_mask(mapunits)

Build a mask for non-soil or urban map units.

Parameters:
  • mapunits (DataFrame | GeoDataFrame) –

    Map-unit table to evaluate.

Returns:
  • Series

    Boolean Series where True indicates non-soil or urban classes.

Source code in cycles/ssurgo/ssurgo.py
201
202
203
204
205
206
207
208
209
210
211
212
213
214
def non_soil_mask(self, mapunits: pd.DataFrame | gpd.GeoDataFrame) -> pd.Series:
    """Build a mask for non-soil or urban map units.

    Args:
        mapunits: Map-unit table to evaluate.

    Returns:
        Boolean Series where True indicates non-soil or urban classes.
    """
    return (
        mapunits['mukey'].isna() |
        mapunits['muname'].isin(SSURGO_NON_SOIL_TYPES) |
        mapunits['muname'].str.contains('|'.join(SSURGO_URBAN_TYPES), na=False)
    )

get_soil_profile(*, mukey=None, major_only=True)

Build a soil profile from SSURGO components and horizons.

Parameters:
  • mukey (int | None, default: None ) –

    Optional map-unit key. If omitted, the selected major MUKEY is used.

  • major_only (bool, default: True ) –

    If True, include only components marked as major.

Returns:
  • list[SoilLayer]

    Soil profile as a list of SoilLayer records.

Source code in cycles/ssurgo/ssurgo.py
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
def get_soil_profile(self, *, mukey: int | None=None, major_only: bool=True) -> list[SoilLayer]:
    """Build a soil profile from SSURGO components and horizons.

    Args:
        mukey: Optional map-unit key. If omitted, the selected major MUKEY is used.
        major_only: If True, include only components marked as major.

    Returns:
        Soil profile as a list of ``SoilLayer`` records.
    """
    mukey = mukey or self._ensure_mukey()

    df = self.components[self.components['mukey'] == int(mukey)].copy()
    if major_only:
        df = df[df['majcompflag'] == 'Yes']

    df = pd.merge(df, self.horizons, on='cokey').query("hzname != 'R'").sort_values(by=['cokey', 'top'], ignore_index=True)

    return [SoilLayer(
            top = row['top'],
            bottom = row['bottom'],
            **{p: None if pd.isna(row[p]) else row[p] for p in MAPPABLE_PARAMETERS},    # type: ignore
        ) for _, row in df.iterrows()]

generate_soil_file(fn, *, mukey=None, desc=None, hsg=None, slope=None, soil_depth=None)

Generate a Cycles soil file from SSURGO profile data.

Parameters:
  • fn (Path | str) –

    Output soil file path.

  • mukey (int | None, default: None ) –

    Optional map-unit key. If omitted, dominant MUKEY is used.

  • desc (str | None, default: None ) –

    Optional custom header text for the output file.

  • hsg (str | None, default: None ) –

    Optional hydrologic soil group; inferred from map unit if omitted.

  • slope (float | None, default: None ) –

    Optional slope value; inferred from map unit if omitted.

  • soil_depth (float | None, default: None ) –

    Optional maximum depth (m) used during profile mapping.

Returns:
  • None

    None.

Source code in cycles/ssurgo/ssurgo.py
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
def generate_soil_file(self, fn: Path | str, *,
    mukey: int | None=None, desc: str | None=None, hsg: str | None=None, slope: float | None=None, soil_depth: float | None=None) -> None:
    """Generate a Cycles soil file from SSURGO profile data.

    Args:
        fn: Output soil file path.
        mukey: Optional map-unit key. If omitted, dominant MUKEY is used.
        desc: Optional custom header text for the output file.
        hsg: Optional hydrologic soil group; inferred from map unit if omitted.
        slope: Optional slope value; inferred from map unit if omitted.
        soil_depth: Optional maximum depth (m) used during profile mapping.

    Returns:
        None.
    """
    if mukey is not None:
        hsg = self._mapunits[self._mapunits['mukey'] == mukey]['hydgrpdcd'].iloc[0] if hsg is None else hsg
        slope = self._mapunits[self._mapunits['mukey'] == mukey]['slopegradwta'].iloc[0] if slope is None else slope
    else:
        mukey = self._ensure_mukey()
        hsg = self.hsg if hsg is None else hsg
        slope = self.slope if slope is None else slope

    assert mukey is not None and hsg is not None and slope is not None

    profile = self.get_soil_profile(mukey=mukey)
    desc = desc if desc is not None else _build_desc(self._get_muname(mukey), mukey, hsg)
    _generate_soil_file(fn, profile, desc=desc, hsg=hsg, slope=slope, soil_depth=soil_depth)