1 Introduction

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)

2 Examples

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)

2.1 ST Explainer

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                                   

2.2 ST Hierarchy to the Subgroup Level

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         

2.3 ST Hierarchy with Family and Series

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                         

2.4 SoilWeb seriesTree Application

seriesTree figure Link to SoilWeb subgroup taxa tree for abruptic durixeralfs.

2.5 Taxa Extent Maps

# 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)

2.6 Data via soilDB::fetchOSD

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)

2.6.1 Annual climate summaries

# display annual climate summary
trellis.par.set(plot.line=list(col='RoyalBlue'))
print(update(res$fig, layout=c(4,2), sub='Abruptic Durixeralfs'))

2.6.2 Geomorphic Descriptions

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')

2.7 KSSL Data

# 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
)

2.7.1 Morphologic Summaries

by generalized horizon labels, depth intervals, ???.

2.7.2 ST Criteria via {aqp}

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)

2.8 SoilWeb metadata

kable_styling(kable(osd$soilweb.metadata, format = 'html', caption = 'SoilWeb Snapshot Metadata'), full_width = FALSE, font_size = 10)
SoilWeb Snapshot Metadata
product last_update
block diagram archive 2019-12-17
cached sketches 2023-10-12
component pedons 2023-10-09
ESID-series cross-tabulation 2023-11-03
ESID/series cross-tabulation 2023-11-06
KSSL snapshot 2020-03-18
MLRA membership 2023-10-06
national musym LUT 2023-10-04
OSD fulltext 2023-10-06
OSD morphology 2023-10-06
SC database 2023-10-02
SDE cached figures 2023-10-12
series climate summary 2023-10-09
series-ESID cross-tabulation 2023-11-03
series/ESID cross-tabulation 2023-11-06
series extent 2023-10-04
series monthly water balance 2023-10-12
SeriesTree 2023-10-06
SSA vintage 2023-10-04
SSURGO geomorphology 2023-10-04
SSURGO NCCPI Stats 2022-10-11
SSURGO parent material 2023-10-04
SSURGO SOC Stocks 2023-10-04
taxa extent 2022-10-20

This document is based on aqp version 2.0.3 and soilDB version 2.8.1.