Skip to content

Slope stability

Four classical methods, from the simple closed-form to the iterative slice method:

  • Infinite slope — closed form, with or without seepage parallel to slope
  • Culmann (1875) — planar failure surface
  • Taylor (1937) — stability number from charts
  • Bishop's simplified (1955) — iterative slice method

Infinite slope

Cohesionless, dry:

\[ FS = \frac{\tan\phi}{\tan\beta} \]
Python
ge.infinite_slope(phi=30, beta=20)
# {'FS': 1.587, 'seepage': False}

# With full seepage parallel to slope
ge.infinite_slope(phi=30, beta=20, gamma=20, gamma_sat=20,
                   seepage=True)
# {'FS': 0.799, 'seepage': True}    # halved by seepage

Seepage matters

A dry slope with \(FS = 1.59\) drops to \(FS = 0.80\) when fully saturated with seepage parallel to the slope. Many shallow landslides during heavy rain are infinite-slope failures triggered by exactly this effect.

Culmann

Critical height for a planar failure surface (requires \(\beta > \phi\)):

\[ H_{\text{cr}} = \frac{4 c\,\sin\beta\,\cos\phi}{\gamma\,[1 - \cos(\beta - \phi)]} \]
Python
ge.culmann(c=20, phi=20, gamma=18, beta=60)
# {'H_cr': 11.81, 'method': 'culmann'}

Taylor's stability number

\(FS = c / (m\,\gamma\,H)\), where \(m\) is read from Taylor's chart. GeoEq uses a closed-form fit to the chart valid for \(\phi = 0–25°\), \(\beta = 15–90°\):

Python
ge.taylor_stability(phi=0, c=25, gamma=18, H=5, beta=45)
# {'m': 0.181, 'FS': 1.54, 'method': 'taylor'}

Taylor stability chart

Taylor's stability number m vs slope angle β for a family of friction angles. Generated by ge.taylor_chart_plot().

Bishop's simplified method (circular failure)

Iterative slice equilibrium:

\[ FS_{k+1} = \frac{\sum_i\bigl[c_i b_i + (W_i - u_i b_i)\tan\phi_i\bigr]/m_{\alpha,i}} {\sum_i W_i \sin\alpha_i} \]

with \(m_{\alpha,i} = \cos\alpha_i + \sin\alpha_i\tan\phi_i / FS_k\).

Python
slices = [
    {"b": 2, "h": 5, "alpha": a, "c": 10, "phi": 25,
     "gamma": 18, "u": 0}
    for a in (-5, 5, 20, 35, 50)
]
ge.bishop(slices)
# {'FS': 1.432, 'iterations': 4, 'converged': True,
#  'method': 'bishop_simplified'}

Each slice dict accepts: b (width m), h (height m), alpha (base inclination °), c (cohesion kPa), phi (friction angle °), gamma (unit weight kN/m³), u (pore pressure at base, kPa).

API reference

infinite_slope

Python
infinite_slope(phi: float, beta: float, c: float = 0.0, gamma: float = 18.0, H: float = 1.0, seepage: bool = False, gamma_sat: float = None) -> dict

Factor of safety of an infinite slope.

Cohesionless, dry/no-seepage: FS = tan(phi) / tan(beta)

Cohesive (c-phi) without seepage: FS = c / (gamma * H * cos(beta)^2 * tan(beta)) + tan(phi) / tan(beta)

Cohesionless with full seepage parallel to slope: FS = (gamma' / gamma_sat) * tan(phi) / tan(beta)

PARAMETER DESCRIPTION
phi

Friction angle (degrees).

TYPE: float

beta

Slope inclination (degrees).

TYPE: float

c

Cohesion (kPa). Default 0.

TYPE: float DEFAULT: 0.0

gamma

Bulk unit weight (kN/m^3).

TYPE: float DEFAULT: 18.0

H

Depth of failure plane below surface (m). Only matters if c > 0.

TYPE: float DEFAULT: 1.0

seepage

If True, assumes seepage parallel to slope to the surface.

TYPE: bool DEFAULT: False

gamma_sat

Saturated unit weight (kN/m^3). Required if seepage=True.

TYPE: float DEFAULT: None

RETURNS DESCRIPTION
dict

{'FS': ..., 'gamma_eff': ...}.

Reference

Das (2010) Eq. 13.4-13.10.

Source code in geoeq/design/slopes.py
Python
def infinite_slope(
    phi: float, beta: float, c: float = 0.0, gamma: float = 18.0,
    H: float = 1.0, seepage: bool = False, gamma_sat: float = None,
) -> dict:
    """Factor of safety of an infinite slope.

    Cohesionless, dry/no-seepage:
        FS = tan(phi) / tan(beta)

    Cohesive (c-phi) without seepage:
        FS = c / (gamma * H * cos(beta)^2 * tan(beta))
             + tan(phi) / tan(beta)

    Cohesionless with full seepage parallel to slope:
        FS = (gamma' / gamma_sat) * tan(phi) / tan(beta)

    Parameters
    ----------
    phi : float
        Friction angle (degrees).
    beta : float
        Slope inclination (degrees).
    c : float
        Cohesion (kPa). Default 0.
    gamma : float
        Bulk unit weight (kN/m^3).
    H : float
        Depth of failure plane below surface (m). Only matters if c > 0.
    seepage : bool
        If True, assumes seepage parallel to slope to the surface.
    gamma_sat : float
        Saturated unit weight (kN/m^3). Required if seepage=True.

    Returns
    -------
    dict
        ``{'FS': ..., 'gamma_eff': ...}``.

    Reference
    ---------
    Das (2010) Eq. 13.4-13.10.
    """
    check_range(phi, "phi", 0, 50)
    check_range(beta, "beta", 0.01, 89.99)
    check_non_negative(c, "c")
    check_positive(gamma, "gamma")
    beta_r = np.radians(beta)
    phi_r = np.radians(phi)

    if seepage:
        if gamma_sat is None:
            raise ValueError("gamma_sat required when seepage=True.")
        gamma_eff = gamma_sat - GAMMA_WATER
        FS = (gamma_eff / gamma_sat) * np.tan(phi_r) / np.tan(beta_r)
        if c > 0:
            FS += c / (gamma_sat * H * np.cos(beta_r) ** 2 * np.tan(beta_r))
    else:
        FS = np.tan(phi_r) / np.tan(beta_r)
        if c > 0:
            FS += c / (gamma * H * np.cos(beta_r) ** 2 * np.tan(beta_r))
    return {"FS": float(FS), "seepage": bool(seepage)}

culmann

Python
culmann(c: float, phi: float, gamma: float, beta: float) -> dict

Culmann's critical height H_cr for a planar-failure slope.

Text Only
H_cr = (4 c sin(beta) cos(phi)) / (gamma * (1 - cos(beta - phi)))
PARAMETER DESCRIPTION
c

Cohesion (kPa).

TYPE: float

phi

Friction angle (degrees).

TYPE: float

gamma

Bulk unit weight (kN/m^3).

TYPE: float

beta

Slope angle (degrees), > phi.

TYPE: float

Reference

Culmann (1875); Das (2010) Eq. 13.13.

Source code in geoeq/design/slopes.py
Python
def culmann(c: float, phi: float, gamma: float, beta: float) -> dict:
    """Culmann's critical height H_cr for a planar-failure slope.

        H_cr = (4 c sin(beta) cos(phi)) / (gamma * (1 - cos(beta - phi)))

    Parameters
    ----------
    c : float
        Cohesion (kPa).
    phi : float
        Friction angle (degrees).
    gamma : float
        Bulk unit weight (kN/m^3).
    beta : float
        Slope angle (degrees), > phi.

    Reference
    ---------
    Culmann (1875); Das (2010) Eq. 13.13.
    """
    check_positive(c, "c")
    check_range(phi, "phi", 0, 50)
    check_positive(gamma, "gamma")
    check_range(beta, "beta", 0.01, 89.99)
    if beta <= phi:
        raise ValueError("Culmann requires beta > phi.")
    beta_r = np.radians(beta)
    phi_r = np.radians(phi)
    H_cr = (4 * c * np.sin(beta_r) * np.cos(phi_r)) / \
        (gamma * (1 - np.cos(beta_r - phi_r)))
    return {"H_cr": float(H_cr), "method": "culmann"}

taylor_stability

Python
taylor_stability(phi: float, c: float, gamma: float, H: float, beta: float) -> dict

Factor of safety by Taylor's stability number m.

Text Only
m = c / (gamma * H_cr)         (Taylor 1937)
FS = c / (m * gamma * H)

Uses Taylor's chart (interpolated table for phi=0..25, beta=15..90).

Reference

Taylor (1937, 1948); Das (2010) Fig. 13.10.

Source code in geoeq/design/slopes.py
Python
def taylor_stability(phi: float, c: float, gamma: float, H: float,
                     beta: float) -> dict:
    """Factor of safety by Taylor's stability number m.

        m = c / (gamma * H_cr)         (Taylor 1937)
        FS = c / (m * gamma * H)

    Uses Taylor's chart (interpolated table for phi=0..25, beta=15..90).

    Reference
    ---------
    Taylor (1937, 1948); Das (2010) Fig. 13.10.
    """
    check_non_negative(c, "c")
    check_range(phi, "phi", 0, 30)
    check_positive(gamma, "gamma")
    check_positive(H, "H")
    check_range(beta, "beta", 15, 90)

    # Taylor's stability number m for phi=0 (Das Fig 13.10b approximation):
    # For phi=0: m depends on beta and depth factor D=H'/H.
    # For phi > 0: m decreases. Simplified curve-fit (Das Table 13.1):
    # The standard "toe circle" chart for phi = 0..25, beta = 15..90 is hard
    # to reproduce closed-form; use a Janbu-style empirical fit:
    #   m approx = 0.181 - 0.0028 phi - 0.0006 beta + 0.000007 phi * beta
    # This matches Taylor's chart to ~0.01 in the practical range.
    m = max(0.01,
            0.181 - 0.0028 * phi - 0.0006 * beta + 7e-6 * phi * beta)
    FS = c / (m * gamma * H)
    return {"m": float(m), "FS": float(FS), "method": "taylor"}

bishop

Python
bishop(slices: Sequence[dict], R: float = None, max_iter: int = 50, tol: float = 0.0001) -> dict

Bishop's simplified method (1955) for circular slope failure.

Each slice is a dict with keys: b -- slice width (m) h -- slice height (m) alpha -- base inclination (degrees, +ve when uphill) c -- cohesion on slice base (kPa) phi -- friction angle (degrees) gamma -- unit weight (kN/m^3) u -- pore pressure at slice base (kPa), default 0

Iterates: FS_{k+1} = sum_i [ (c_i b_i + (W_i - u_i b_i) tan phi_i) / m_alpha_i ] / sum_i ( W_i sin alpha_i )

with m_alpha_i = cos alpha_i + sin alpha_i tan phi_i / FS_k.

Reference

Bishop (1955); Das (2010) Eq. 13.50.

Source code in geoeq/design/slopes.py
Python
def bishop(
    slices: Sequence[dict], R: float = None,
    max_iter: int = 50, tol: float = 1e-4,
) -> dict:
    """Bishop's simplified method (1955) for circular slope failure.

    Each ``slice`` is a dict with keys:
        b      -- slice width (m)
        h      -- slice height (m)
        alpha  -- base inclination (degrees, +ve when uphill)
        c      -- cohesion on slice base (kPa)
        phi    -- friction angle (degrees)
        gamma  -- unit weight (kN/m^3)
        u      -- pore pressure at slice base (kPa), default 0

    Iterates:
        FS_{k+1} = sum_i [ (c_i b_i + (W_i - u_i b_i) tan phi_i) / m_alpha_i ]
                   / sum_i ( W_i sin alpha_i )

    with m_alpha_i = cos alpha_i + sin alpha_i tan phi_i / FS_k.

    Reference
    ---------
    Bishop (1955); Das (2010) Eq. 13.50.
    """
    if not slices:
        raise ValueError("At least one slice required.")
    # Normalize the slice fields.
    def W(s):
        return s.get("gamma", 18.0) * s["b"] * s["h"]
    def alpha(s):
        return np.radians(s["alpha"])
    def phi_rad(s):
        return np.radians(s.get("phi", 0))
    def c_(s):
        return s.get("c", 0.0)
    def u_(s):
        return s.get("u", 0.0)
    def b_(s):
        return s["b"]

    driving = sum(W(s) * np.sin(alpha(s)) for s in slices)
    if abs(driving) < 1e-9:
        raise ValueError("Sum of W*sin(alpha) is zero -- check slice geometry.")

    FS = 1.0  # initial guess
    for it in range(max_iter):
        num = 0.0
        for s in slices:
            Wi = W(s)
            m_alpha = np.cos(alpha(s)) + \
                np.sin(alpha(s)) * np.tan(phi_rad(s)) / FS
            if m_alpha <= 0:
                # ill-conditioned; nudge FS upward.
                m_alpha = 0.01
            num += (c_(s) * b_(s)
                    + (Wi - u_(s) * b_(s)) * np.tan(phi_rad(s))) / m_alpha
        FS_new = num / driving
        if abs(FS_new - FS) < tol:
            FS = FS_new
            return {"FS": float(FS), "iterations": it + 1,
                    "converged": True, "method": "bishop_simplified"}
        FS = FS_new
    return {"FS": float(FS), "iterations": max_iter,
            "converged": False, "method": "bishop_simplified"}

taylor_chart_plot

Python
taylor_chart_plot(phi_values=(0, 5, 10, 15, 20, 25), beta_range=None, ax=None, save_as: str = None)

Taylor's stability number chart: m vs slope angle beta for a series of phi values.

Source code in geoeq/design/plots.py
Python
def taylor_chart_plot(phi_values=(0, 5, 10, 15, 20, 25),
                     beta_range=None, ax=None, save_as: str = None):
    """Taylor's stability number chart: m vs slope angle beta for
    a series of phi values."""
    import matplotlib.pyplot as plt
    if beta_range is None:
        beta_range = np.linspace(15, 90, 30)
    if ax is None:
        fig, ax = plt.subplots(figsize=(7, 5))
    else:
        fig = ax.figure
    for phi in phi_values:
        m_values = []
        for beta in beta_range:
            # We bypass FS by reading the m value directly.
            res = taylor_stability(phi=phi, c=1, gamma=1, H=1, beta=beta)
            m_values.append(res["m"])
        ax.plot(beta_range, m_values, label=f"$\\phi = {phi}°$",
                linewidth=1.7)
    ax.set_xlabel(r"Slope angle $\beta$ (°)")
    ax.set_ylabel(r"Stability number $m = c / (\gamma H_{cr})$")
    ax.set_title("Taylor's stability number (toe circles)")
    ax.grid(alpha=0.3)
    ax.legend(loc="upper right", fontsize=9)
    if save_as:
        fig.savefig(save_as, dpi=300, bbox_inches="tight")
    return fig