Soil Taxonomy is a theoretical framework for organizing soil information, typically at scales coarser than most land management decisions. Soil series are a practical (but limited in geographic scope) framework for organizing soil/landscape combinations close to the scale of land management decisions.
So, what is the practical manifestation of a single subgroup from Soil Taxonomy (abruptic durixeralfs), in terms of soil series concepts?
The following code, links, and notes may serve as a starting point for those interested in learning more about Soil Taxonomy. This is a work in progress.
# AQP suite, be sure to use the development versions from GitHub
library(aqp)
library(soilDB)
library(sharpshootR)
# used for visualization
library(latticeExtra)
library(tactile)
library(cluster)
library(ape)
library(FactoMineR)
# mapping a subgroup
library(sf)
library(terra)
library(spData)
library(rasterVis)
library(viridisLite)
# subgroup "explainer"
library(SoilTaxonomy)
library(data.tree)
Get a snapshot of the Soil Classification database. This contains the current classification for all soil series.
# latest version, synced quarterly
u <- 'https://github.com/ncss-tech/SoilWeb-data/raw/main/files/SC-database.csv.gz'
tf <- tempfile()
download.file(u, destfile = tf)
SC <- read.csv(tf, stringsAsFactors = FALSE)
Identify all of the soil series associated with a specific subgroup
taxa: abruptic durixeralfs and download basic morphology and
climate summaries via fetchOSD.
# select series names for a single subgroup
SC <- SC[which(SC$taxsubgrp == 'abruptic durixeralfs'), ]
s <- SC$soilseriesname
# get OSD morphology and extended summaries
osd <- fetchOSD(soils = s, extended = TRUE)
Explain subgroup taxa via SoilTaxonomy package.
cat(explainST('abruptic durixeralfs'))
abruptic durixeralfs
| | | |
abrupt textural change
| | |
presence of a duripan
| |
xeric soil moisture regime
|
soils with an argillic, kandic, or natric horizon
Automatic to the subgroup, via SoilTaxonomy package.
taxonTree(c('durixeralfs', 'rhodoxeralfs'), special.chars = c("\u251c","\u2502", "\u2514", "\u2500 "))
Soil Taxonomy
└─ alfisols
└─ xeralfs
├─ durixeralfs
│ ├─ natric durixeralfs
│ ├─ vertic durixeralfs
│ ├─ aquic durixeralfs
│ ├─ abruptic haplic durixeralfs
│ ├─ abruptic durixeralfs
│ ├─ haplic durixeralfs
│ └─ typic durixeralfs
└─ rhodoxeralfs
├─ lithic rhodoxeralfs
├─ vertic rhodoxeralfs
├─ petrocalcic rhodoxeralfs
├─ calcic rhodoxeralfs
├─ inceptic rhodoxeralfs
└─ typic rhodoxeralfs
We have to do this manually.
# subset SC database to required columns
SC <- SC[, c('soilseriesname', 'taxorder', 'taxsuborder', 'taxgrtgroup', 'taxsubgrp', 'taxclname')]
# normalization via lower case
SC$taxclname <- tolower(SC$taxclname)
SC$soilseriesname <- tolower(SC$soilseriesname)
# remove subgroup component of family spec
SC$f <- NA_character_
for(i in 1:nrow(SC)) {
SC$f[i] <- gsub(pattern = SC$taxsubgrp[i], replacement = '', SC$taxclname[i], fixed = TRUE)
}
# remove white space
SC$f <- trimws(SC$f, which = 'both')
# required columns only, smaller data.tree
v <- c('taxorder', 'taxsuborder', 'taxgrtgroup', 'taxsubgrp', 'f', 'soilseriesname', 'path')
# init data.tree object
SC$path <- sprintf("ST/%s/%s/%s/%s/%s/%s", SC$taxorder, SC$taxsuborder, SC$taxgrtgroup, SC$taxsubgrp, SC$f, SC$soilseriesname)
n <- as.Node(SC[, v], pathName = 'path')
## missing family / series result in an ugly tree, prune accordingly
# prune missing family / series
pf <- function(i) {
# NA due to left join
# note odd approach required, matching to 'NA' vs. is.na()
if(GetAttribute(i, 'name') == 'NA') {
return(FALSE)
} else {
return(TRUE)
}
}
## print
# print(n, limit = NULL, pruneFun = pf)
levelName
1 ST
2 °--alfisols
3 °--xeralfs
4 °--durixeralfs
5 °--abruptic durixeralfs
6 ¦--fine-loamy, mixed, superactive, thermic
7 ¦ °--bellota
8 ¦--ashy, glassy, frigid
9 ¦ °--borealis
10 ¦--fine, kaolinitic, thermic
11 ¦ °--chesterton
12 ¦--clayey-skeletal, kaolinitic, thermic
13 ¦ °--clough
14 ¦--very-fine, smectitic, frigid
15 ¦ °--deunah
16 ¦--fine, mixed, active, thermic
17 ¦ ¦--eastbiggs
18 ¦ ¦--redding
19 ¦ ¦--san joaquin
20 ¦ °--yuvas
21 ¦--fine, smectitic, mesic
22 ¦ °--fairylawn
23 ¦--clayey, smectitic, frigid, shallow
24 ¦ ¦--furshur
25 ¦ ¦--nicholflat
26 ¦ °--ponina
27 ¦--fine, illitic, thermic
28 ¦ °--gloria
29 ¦--clayey, mixed, active, thermic, shallow
30 ¦ ¦--keyes
31 ¦ °--vistarobles
32 ¦--fine, smectitic, thermic
33 ¦ °--madera
34 ¦--fine, vermiculitic, thermic
35 ¦ °--moda
36 ¦--fine, smectitic, frigid
37 ¦ °--thacker
38 °--fine-loamy, mixed, active, thermic
39 °--thermalito
Link to SoilWeb subgroup taxa tree for abruptic
durixeralfs.
# get 800m extent map
e <- taxaExtent('abruptic durixeralfs', level = 'subgroup')
# aggregate via focal mean using 5x5 moving window
# visualization purposes only
a <- terra::aggregate(e, fact = 5, fun = mean, na.rm = TRUE)
data("us_states")
us_states <- st_transform(us_states, 5070)
us_states <- st_crop(us_states, e)
# simple figure
plot(a, axes = FALSE, col = mako(50), mar = c(3, 1, 3, 3))
plot(st_geometry(us_states), add = TRUE)
title('Abruptic Durixeralfs')
mtext('pixel values represent percent of area\nwithin 800m grid cells', side = 1, line = 1.5)
Cluster series based on annual climate summaries but don’t plot it yet.
# control centers symbol and size here
res <- vizAnnualClimate(osd$climate.annual, s = 'SAN JOAQUIN', IQR.cex = 1.1, cex=1.1, pch=18)
Series associated with abruptic durixeralfs subgroup, arranged according to annual climate summaries.
par(mar = c(0,0,1,2))
plotProfileDendrogram(osd$SPC, clust = res$clust, scaling.factor = 0.075, width = 0.25, y.offset = 0.5, name.style = 'center-center', shrink = TRUE, max.depth = 150, depth.axis = list(cex = 0.8))
mtext('Abruptic Durixeralfs', side = 1, at = 0.5, adj = 0, line = -1.5, font=4)
mtext('sorted by annual climate summaries', side = 3, at = 0.5, adj = 0, line = -1.5, font = 1, cex = 1.1)
# display annual climate summary
trellis.par.set(plot.line=list(col='RoyalBlue'))
print(update(res$fig, layout=c(4,2), sub='Abruptic Durixeralfs'))
hs <- vizHillslopePosition(osd$hillpos)
print(hs$fig)
gc <- vizGeomorphicComponent(osd$geomcomp)
print(gc$fig)
tr <- vizTerracePosition(osd$terrace)
print(tr$fig)
hs.tab <- (osd$hillpos[, 2:6])
row.names(hs.tab) <- osd$hillpos$series
gc.tab <- (osd$geomcomp[, 2:7])
row.names(gc.tab) <- osd$geomcomp$series
hs.ca <- CA(hs.tab, graph = FALSE)
gc.ca <- CA(gc.tab, graph = FALSE)
plot(hs.ca, autoLab='yes', title='Hillslope Position', cex=0.75, col.col='firebrick', col.row='royalblue')
plot(gc.ca, autoLab='yes', title='Geomorphic Component', cex=0.75, col.col='firebrick', col.row='royalblue')
# latest LDM snapshot, no soil morphologic data
lab <- fetchLDM(
WHERE = "CASE WHEN corr_taxsubgrp IS NOT NULL THEN LOWER(corr_taxsubgrp) ELSE LOWER(samp_taxsubgrp) END = 'abruptic durixeralfs' "
)
# remove any profiles with horizon depth logic
lab <- HzDepthLogicSubset(lab)
length(lab)
# truncate profiles at 200cm
lab <- trunc(lab, 0, 200)
# # sanity check
# par(mar = c(0, 0, 3, 2))
# plotSPC(lab[1:15, ], print.id = FALSE, name = NA, width = 0.33, color = 'clay_total')
# weighted-mean particle diameter
lab$wmpd <- with(
horizons(lab), ((sand_very_coarse * 1.5) + (sand_coarse * 0.75) + (sand_medium * 0.375) + (sand_fine * 0.175) + (sand_very_fine *0.075) + (silt_total * 0.026) + (clay_total * 0.0015)) / (sand_very_coarse + sand_coarse + sand_medium + sand_fine + sand_very_fine + silt_total + clay_total))
# estimate soil depth based on horizon designations
sdc <- getSoilDepthClass(lab)
# splice-into site data
site(lab) <- sdc
site(lab)$.grp <- factor('Abruptic Durixeralfs')
a <- slab(lab, .grp ~ clay_total + wmpd + cole_whole_soil + ph_h2o + base_sat_sum_of_cations_ph_8_2)
# re-name soil properties for clarity
a$variable <- factor(
a$variable,
labels = c('Total Clay (%)', 'WMPD', 'COLE WS (%)', 'pH 1:1 H2O', 'Base Saturation pH 8.2 (%)')
)
# define plotting style
tps <- tactile.theme(superpose.line=list(col=c('RoyalBlue', 'DarkRed', 'DarkGreen'), lwd=2))
# plot grouped, aggregate data
xyplot(top ~ p.q50 | variable,
groups = .grp,
data = a,
ylab='Depth',
xlab='median bounded by 5th and 95th percentiles',
main = 'Abruptic Durixeralfs',
lower=a$p.q5, upper=a$p.q95, ylim=c(155,-5),
panel=panel.depth_function, alpha=0.25, sync.colors=TRUE,
prepanel=prepanel.depth_function,
cf=a$contributing_fraction,
par.strip.text=list(cex=0.8),
strip=strip.custom(bg=grey(0.85)),
layout=c(5,1),
scales=list(x=list(alternating=1, relation='free'), y=list(alternating=3)),
par.settings=tps
)
by generalized horizon labels, depth intervals, ???.
hasDarkColors(lab.spc, dhuenm = 'd_hue', dvalnm = 'd_value', dchrnm = 'd_chroma', mhuenm = 'm_hue', mvalnm = 'm_value', mchrnm = 'm_chroma')
profileApply(lab.spc, getArgillicBounds, clay.attr = 'clay', texcl.attr = 'lab_texture_class', simplify = FALSE)
kable_styling(kable(osd$soilweb.metadata, format = 'html', caption = 'SoilWeb Snapshot Metadata'), full_width = FALSE, font_size = 10)
| product | last_update |
|---|---|
| block diagram archive | 2025-01-21 |
| cached sketches | 2024-10-24 |
| component pedons | 2025-08-05 |
| ESID-series cross-tabulation | 2024-04-11 |
| ISSR800 | 2024-10-30 |
| KSSL snapshot | 2025-01-21 |
| MLRA membership | 2025-01-22 |
| mukey MLRA membership | 2025-01-22 |
| national musym LUT | 2024-10-03 |
| nationalmusym MLRA membership | 2025-01-22 |
| OSD fulltext | 2025-05-30 |
| OSD morphology | 2025-05-30 |
| SC database | 2025-05-29 |
| SDE cached figures | 2024-10-24 |
| series climate summary | 2024-10-23 |
| series-ESID cross-tabulation | 2024-04-11 |
| series extent | 2024-10-18 |
| series monthly water balance | 2024-10-24 |
| SeriesTree | 2025-01-22 |
| SSA vintage | 2024-10-18 |
| SSURGO geomorphology | 2024-10-18 |
| SSURGO NCCPI Stats | 2024-10-23 |
| SSURGO parent material | 2024-10-18 |
| SSURGO SOC Stocks | 2024-10-18 |
| taxa extent | 2024-10-30 |
This document is based on aqp version 2.2-1 and
soilDB version 2.8.12.