Take a fistful of soil. Hold it up to the light. What you have, by volume, is three phases mixed together:
- Solid mineral grains (Vs) — quartz, feldspar, clay platelets — the material that gives the soil its skeleton.
- Water (Vw) — filling the pore spaces, partially or completely.
- Air (Va) — filling whatever pore space the water did not.
The non-solid space is the void volume Vv = Vw + Va. The total volume is V = Vs + Vv. From these few quantities, every other phase parameter — void ratio, porosity, saturation, water content, unit weight — is just a ratio.
Try it yourself
Drag the sliders. The 3-D column updates live, and so do the derived parameters. Notice how raising the void ratio (more void space) lowers the unit weight, and how raising saturation (more water in those voids) raises it again.
The five phase parameters, defined
Every textbook starts here. The same five ratios — written exactly the same way in Das, Bowles, Holtz & Kovacs, Budhu, Craig. We are not inventing anything; we are giving you the simplest API to compute them.
Void ratio (e)
Dimensionless. Typical values: 0.3 for dense gravel, 0.65 for medium sand, 0.9–1.5 for soft to sensitive clays, up to 3.0 for organic peats. Void ratio can exceed 1 — there is genuinely more void space than solid.
import geoeq as ge
e = ge.void_ratio(n=0.42) # 0.7241 — from porosity
e = ge.void_ratio(Vv=72, Vs=100) # 0.72 — from volumes
Porosity (n)
The fraction of the total volume that is void. Always between 0 and 1. A dense sand might have n ≈ 0.30; a soft clay n ≈ 0.55.
n = ge.porosity(e=0.72) # 0.4186
Degree of saturation (S)
The fraction of the void space filled with water. S = 0 is a perfectly dry soil; S = 1 is fully saturated (which is what we assume below the water table for most engineering calculations).
S = ge.saturation(w=0.18, Gs=2.65, e=0.72)
# 0.6625 — 66% saturated
Water content (w)
A mass ratio — water mass divided by dry-solids mass. Get it by weighing a moist sample, oven-drying it overnight at 105 °C, weighing again. Typical values: 5–10 % for compacted gravels, 15–35 % for in-situ clays, >50 % for sensitive soft clays.
w = ge.water_content(S=1.0, Gs=2.65, e=0.72)
# 0.2717 — w at full saturation
Specific gravity of solids (Gs)
The density of the solid mineral grains, normalised by the density of water. Quartz sand: 2.65. Most clays: 2.70–2.75. Iron ores: 3.0+. Measured with a pycnometer.
Gs = ge.specific_gravity(Ms=265.0, Vs=100.0) # 2.65
The one identity that ties them all together
w · Gs = S · e
This is the master equation. Given any three of (w, Gs, S, e)
you can solve for the fourth without ever touching a volume or mass
directly. Memorise it.
GeoEq's ge.saturation() and ge.water_content()
functions are literally just rearranged copies of this identity:
# If we know w, Gs, e -- find S
ge.saturation(w=0.20, Gs=2.70, e=0.85)
# = w*Gs/e = 0.20 * 2.70 / 0.85 = 0.635
# If we know S, Gs, e -- find w
ge.water_content(S=0.80, Gs=2.70, e=0.85)
# = S*e/Gs = 0.80 * 0.85 / 2.70 = 0.252
Unit weights — the four-in-one function
Once we have e, S, and Gs, we
can compute any of four unit weights. Textbooks teach them as four
separate formulas; GeoEq's ge.density() picks the right one
based on a single kind= argument.
γsat = (Gs + e) γw / (1 + e) (saturated, S=1)
γ = (Gs + Se) γw / (1 + e) (bulk, general S)
γ' = γsat − γw (submerged / buoyant)
γ_d = ge.density(Gs=2.65, e=0.72, kind="dry", unit="kN/m3")
γ_s = ge.density(Gs=2.65, e=0.72, kind="saturated", unit="kN/m3")
γ_b = ge.density(Gs=2.65, e=0.72, S=0.8, kind="bulk", unit="pcf")
γ_sub = ge.density(Gs=2.65, e=0.72, kind="submerged", unit="kN/m3")
# 15.10 kN/m³, 19.23 kN/m³, 117.3 pcf, 9.42 kN/m³
A worked example — undisturbed clay sample
A clay specimen weighs 1010 g in the wet state and 800 g after oven drying. Its bulk volume is 600 cm³, and the specific gravity of solids is 2.72. Determine w, e, n, and S.
import geoeq as ge
# Step 1 — water content (direct from masses)
w = (1010 - 800) / 800 # 0.2625
# Step 2 — volume of solids (mass / density)
Vs = 800 / 2.72 # 294.12 cm³
Vv = 600 - Vs # 305.88 cm³
# Step 3 — phase ratios
e = ge.void_ratio(Vv=Vv, Vs=Vs) # 1.040
n = ge.porosity(e=e) # 0.510
S = ge.saturation(w=w, Gs=2.72, e=e) # 0.687
print(f"w={w:.3f}, e={e:.3f}, n={n:.3f}, S={S:.3f}")
# w=0.262, e=1.040, n=0.510, S=0.687
That's the whole loop. Three lines of unique work, plus a couple of unit-conversions. In a spreadsheet, this would be six cells with hand-typed formulas; here it's traceable, validated code that you can re-run a year from now and get the same answer.
Why this matters for everything else
Every later calculation in soil mechanics consumes one or more of these five parameters. A bearing-capacity check needs γ. A settlement prediction needs e. A seepage analysis needs n. A liquefaction triggering analysis needs σ'v, which is γ-integrated downward.
Get the phase relationships wrong, and every downstream number is
wrong. Get them right — once, in code — and they propagate cleanly
through the rest of the workflow. That is the entire reason for
ge.SoilProfile: define each layer's phase parameters once,
and stress, settlement, liquefaction-triggering all consume them
automatically.