API Reference - Solve Module

bishop = _with_nonlinear_strength(bishop) module-attribute

Computes FS using the complete Bishop's Simplified Method (Equation 10) and computes N_eff (Equation 8). Requires circular slip surface and full input data structure consistent with OMS.

Parameters:
  • slice_df

    pandas.DataFrame with required columns (see OMS spec)

  • debug

    bool, if True prints diagnostic info

  • tol

    float, convergence tolerance

  • max_iter

    int, maximum iteration steps

Returns:
  • (bool, dict | str)

    (True, {'method': 'bishop', 'FS': value}) or (False, error message)

corps = _with_nonlinear_strength(corps) module-attribute

Corps of Engineers (Modified Swedish) force-equilibrium solver.

The interslice forces are assumed parallel at an inclination set by one of the two standard Corps conventions (USACE EM 1110-2-1902). Both are offered by commercial codes (e.g. SLOPE/W "Corps of Engineers #1/#2"):

variant=1: a single constant θ = inclination of the line from the bottom to the top of the failure surface (the crest-to-toe chord). variant=2 (default): θ at each slice boundary parallel to the GROUND SURFACE (slice top).

Variant 2 is the default because it is robust under an unconstrained search (variant 1 uses one fixed θ everywhere and can return a spuriously low FS on surfaces with steep segments, which a self-search exploits). Note that variant 2 is NOT the conservative choice: the ground-parallel convention systematically gives the HIGHEST (least conservative) factor of safety among XSLOPE's methods — typically a few percent to ~15% above Spencer — and the USACE manual itself notes the "average embankment slope" assumption can be unconservative. Report a rigorous method (Spencer) for design; use Corps for comparison.

Both Corps conventions and Lowe & Karafiath set the inter-slice inclination from the signed ground/base slope and negate it on right-facing slopes, so the force-equilibrium factor of safety is mirror-symmetric.

Parameters:
  • slice_df (DataFrame) –

    Must include ['x_l','y_lt','x_r','y_rt'] plus all columns required by force_equilibrium.

  • variant (int) –

    1 = crest-to-toe chord, 2 = per-slice ground-parallel.

Returns:
  • Tuple( (bool, dict or str) ) –

    Whatever force_equilibrium returns.

janbu = _with_nonlinear_strength(janbu) module-attribute

Computes FS using Janbu's Simplified Method with the f0 correction factor.

Janbu's Simplified Method satisfies overall HORIZONTAL FORCE equilibrium of the sliding mass with the inter-slice shear forces neglected. The effective base normal force is obtained from VERTICAL equilibrium of each slice — the same m_alpha = cosα + sinα·tanφ/F relation used by Bishop's method — which makes the factor of safety iterative in F. The base (uncorrected) factor of safety is then multiplied by Janbu's empirical correction factor f0(d/L, soil type), which compensates for neglecting the inter-slice shear.

This is the standard Janbu Simplified formulation (matching e.g. GeoStudio SLOPE/W's F_f at lambda=0). It is NOT the Ordinary/Fellenius normal force — using N = W·cosα − u·dl with a ΣW·sinα denominator (an earlier xslope error) collapses the base FS onto the Ordinary Method of Slices.

Implements the full slice force budget: distributed loads (dload at the load inclination beta), seismic force (kw, horizontal), tension-crack water force (t, horizontal), line reinforcement (p), and pile forces (h_pile/theta_p).

Parameters:
  • slice_df

    pandas.DataFrame with required columns (see OMS spec)

  • debug

    bool, if True prints diagnostic info

  • tol

    float, convergence tolerance on F

  • max_iter

    int, maximum fixed-point iterations

Returns:
  • (bool, dict | str)

    (True, {'method': 'janbu', 'FS': corrected_value, 'fo': correction_factor, 'FS_base': uncorrected_value}) or (False, error message)

lowe = _with_nonlinear_strength(lowe) module-attribute

Lowe-Karafiath limit equilibrium: variable interslice inclinations equal to the average of the top‐and bottom‐surface slopes of the two adjacent slices at each boundary.

mprice = _with_nonlinear_strength(mprice) module-attribute

Morgenstern-Price complete-equilibrium method (Approach A: F_f / F_m crossing).

M-P satisfies BOTH force and moment equilibrium with a VARIABLE interslice-force inclination: tan(θ_j) = λ · f(x_j), where f is a prescribed shape function (see _mp_f_vals) and λ a scalar. Spencer is the special case f ≡ 1 (then θ = arctan λ is constant).

Approach A (Fredlund-Krahn / GLE style): for each λ find F_f(λ) = the FS root of the force residual (Z[n] = 0), and F_m(λ) = the FS root of the moment residual (moment about origin = 0), then find λ* where F_f = F_m. The crossing FS is the M-P factor of safety; λ* fixes the interslice-force distribution. Both residuals come from _mp_march.

Parameters:
  • f_type

    'constant' (== Spencer regression) or 'half_sine' (design default).

  • lambda_bracket

    search interval for λ (sign change of F_f − F_m).

Returns: (True, {'method','FS','lambda','f_type','theta'(deg, per boundary, n+1)}) or (False, message).

Right-facing slopes are handled: _mp_extract mirrors the per-slice arrays, and f ≡ 1 reproduces spencer() to machine precision on both facings.

oms = _with_nonlinear_strength(oms) module-attribute

Computes FS by direct application of Equation 9 (Ordinary Method of Slices).

Inputs

slice_df : pandas.DataFrame Must contain exactly these columns (length = n slices): 'alpha' (deg) = base inclination αᵢ 'phi' (deg) = friction angle φᵢ 'c' = cohesion cᵢ 'w' = slice weight Wᵢ 'u' = pore pressure force/unit‐length on base, uᵢ 'dl' = base length Δℓᵢ 'dload' = resultant distributed load Dᵢ 'd_x','d_y' = centroid (x,y) at which Dᵢ acts 'beta' (deg) = distributed-load inclination βᵢ 'kw' = seismic horizontal kWᵢ 't' = tension‐crack horizontal Tᵢ (zero except one slice) 'y_t' = y‐loc of Tᵢ's line of action (zero except that one slice) 'p' = reinforcement uplift pᵢ (zero if none) 'x_c','y_cg' = slice‐centroid (x,y) for seismic moment arm 'h_pile' = pile/pier force on base (zero if none) 'theta_p' (RAD) = pile force inclination from horizontal — stored and consumed in RADIANS, unlike every other angle here 'r' = radius of circular failure surface 'xo','yo' = x,y coordinates of circle center

Returns

(bool, dict_or_str) • If success: (True, {'method':'oms', 'FS': }) • If denominator → 0 or other fatal error: (False, "")

spencer = _with_nonlinear_strength(spencer) module-attribute

Spencer's Method using Steve G. Wright's formulation from the UTEXAS v2 user manual.

Parameters:
  • slice_df (DataFrame) –

    must contain columns 'alpha' (slice base inclination, degrees), 'phi' (slice friction angle, degrees), 'c' (cohesion), 'dl' (slice base length), 'w' (slice weight), 'u' (pore force per unit length), 'dload' (distributed load), 'beta' (distributed load inclination, degrees), 'kw' (seismic force), 't' (tension crack water force), 'p' (reinforcement force), 'y_lt' (left-top y, used with 'y_lb' by the report-only admissibility screen for the thrust-line check), 'h_pile' (pile force, optional), 'theta_p' (pile inclination, RADIANS, optional)

Returns:
  • float

    FS where FS_force = FS_moment

  • float

    beta (degrees)

  • bool

    converged flag

  • The results dict also carries 'warnings': a list of admissibility

  • notes (base tension on cohesionless slices, interslice tension,

  • thrust line outside the slices) — empty when the solution is clean.

  • Warnings never affect FS or convergence.

force_equilibrium(slice_df, theta_list, fs_guess=1.5, tol=1e-06, max_iter=50, debug=False, right_facing=False)

Limit‐equilibrium by force equilibrium in X & Y with variable interslice angles.

Parameters:
  • slice_df (DataFrame) –

    must contain columns 'alpha' (slice base inclination, degrees), 'phi' (slice friction angle, degrees), 'c' (cohesion), 'dl' (slice base length), 'w' (slice weight), 'u' (pore force per unit length), 'dload' (distributed load), 'beta' (distributed load inclination, degrees), 'kw' (seismic force), 't' (tension crack water force), 'p' (reinforcement force), 'h_pile' (pile force, optional), 'theta_p' (pile inclination, RADIANS, optional)

  • theta_list (array - like) –

    slice‐boundary force inclinations (degrees), length must be n+1 if there are n slices

  • fs_guess (float, default: 1.5 ) –

    initial guess for factor of safety

  • tol (float, default: 1e-06 ) –

    convergence tolerance on residual

  • max_iter (int, default: 50 ) –

    maximum number of Newton (secant) iterations

  • debug (bool, default: False ) –

    print residuals during iteration

Returns:
  • (bool, dict or str)
    • If converged: (True, {'method':'force_equilibrium','FS':})
    • If failed: (False, "error message")
Source code in xslope/solve.py
def force_equilibrium(slice_df, theta_list, fs_guess=1.5, tol=1e-6, max_iter=50, debug=False, right_facing=False):
    """
    Limit‐equilibrium by force equilibrium in X & Y with variable interslice angles.

    Parameters:
        slice_df (pd.DataFrame): must contain columns
            'alpha' (slice base inclination, degrees),
            'phi'   (slice friction angle, degrees),
            'c'     (cohesion),
            'dl'    (slice base length),
            'w'     (slice weight),
            'u'     (pore force per unit length),
            'dload' (distributed load),
            'beta'  (distributed load inclination, degrees),
            'kw'    (seismic force),
            't'     (tension crack water force),
            'p'     (reinforcement force),
            'h_pile' (pile force, optional), 'theta_p' (pile inclination, RADIANS, optional)
        theta_list (array-like): slice‐boundary force inclinations (degrees),
                                 length must be n+1 if there are n slices
        fs_guess (float): initial guess for factor of safety
        tol (float): convergence tolerance on residual
        max_iter (int): maximum number of Newton (secant) iterations
        debug (bool): print residuals during iteration

    Returns:
        (bool, dict or str):
           - If converged: (True, {'method':'force_equilibrium','FS':<value>})
           - If failed:   (False, "error message")
    """
    import numpy as np

    n = len(slice_df)
    if len(theta_list) != n+1:
        return False, f"theta_list length ({len(theta_list)}) must be n+1 ({n+1})"

    # extract and convert to radians
    alpha   = np.radians(slice_df['alpha'].values)
    phi     = np.radians(slice_df['phi'].values)
    c       = _c_eff(slice_df)
    w       = slice_df['w'].values
    u       = slice_df['u'].values
    dl      = slice_df['dl'].values
    D       = slice_df['dload'].values
    beta    = np.radians(slice_df['beta'].values)
    kw      = slice_df['kw'].values
    T       = slice_df['t'].values
    P       = slice_df['p'].values
    theta   = np.radians(np.asarray(theta_list))

    # Pile forces (backward compatible — zeros if no pile data)
    H_pile  = slice_df['h_pile'].values  if 'h_pile'  in slice_df.columns else np.zeros(n)
    theta_p = slice_df['theta_p'].values if 'theta_p' in slice_df.columns else np.zeros(n)

    # v12 support/load terms (all zero when the columns are absent)
    def _c12(name):
        return slice_df[name].values.astype(float) if name in slice_df.columns else np.zeros(n)
    P_pt  = _c12('p_pt')
    pa_cx = _c12('pa_cx'); pa_cy = _c12('pa_cy')
    pp_cx = _c12('pp_cx'); pp_cy = _c12('pp_cy')
    H_pas = _c12('h_pile_pas')
    LL    = _c12('lload')
    ll_b  = np.radians(_c12('ll_beta'))
    # The march mixes the orientation-normalized frame (alpha, theta) with the
    # stored axial components (normalized to cos(psi) > 0). The horizontal
    # component enters as a resisting magnitude (facing-agnostic), but the real
    # tension on a right-facing slope is the negation of the stored direction,
    # so its VERTICAL component flips (same correction as janbu()).
    if right_facing:
        pa_cy = -pa_cy
        pp_cy = -pp_cy

    N = np.zeros(n)  # normal forces on slice bases (filled by each march)
    Z = np.zeros(n+1)  # interslice forces, Z[0] = 0 by definition (no force entering leftmost slice)

    def residual(FS):
        """Right-side interslice force Z[n] for a given FS (force-closure residual).

        Delegates the per-slice march to the shared `_equilibrium_march`, then
        copies its output into the persistent N/Z arrays (via slice assignment) so
        the admissibility check and the slice_df writes after the root-find see the
        values at the converged FS. Pure extraction — arithmetic is unchanged.
        """
        N_march, Z_march = _equilibrium_march(
            alpha, phi, c, w, u, dl, D, beta, kw, T, P, H_pile, theta_p, theta, FS,
            P_pt=P_pt, pa_cx=pa_cx, pa_cy=pa_cy, pp_cx=pp_cx, pp_cy=pp_cy,
            H_pas=H_pas, LL=LL, ll_b=ll_b)
        N[:] = N_march
        Z[:] = Z_march
        return Z[n]

    if debug:
        r0 = residual(fs_guess)
        print(f"FS_guess={fs_guess:.6f} → residual={r0:.4g}")

    # Root-find FS such that the right-end interslice force Z[n] = 0, by Newton-
    # secant from fs_guess. (A bracketed brentq solver was tried to also catch the
    # genuinely-low-FS surfaces where the single-guess secant diverges, but it
    # resurrects non-physical over-strength roots near FS->0 on surfaces where the
    # secant correctly fails, which the admissibility check below does not reliably
    # separate from legitimate low-FS solutions. Deferred.)
    try:
        FS_opt = newton(residual, fs_guess, tol=tol, maxiter=max_iter)
    except Exception as e:
        return False, f"force_equilibrium failed to converge: {e}"

    # A non-positive factor of safety is unphysical: the secant has converged onto a
    # spurious root (it happens on some steep right-facing surfaces). Reject it.
    if not np.isfinite(FS_opt) or FS_opt <= 0:
        return False, f"force_equilibrium: non-physical factor of safety ({FS_opt})"

    # Re-evaluate at the root so N and Z reflect FS_opt (newton's last call may differ).
    residual(FS_opt)

    # Admissibility guard. A free search can drive the force-equilibrium solver onto
    # grossly non-physical surfaces (a large fraction of slices in base tension, or
    # pervasive interslice tension) and report a spurious low FS. Reject those by
    # EXTENT only — a few negative base normals or a non-monotonic thrust line occur
    # in valid solutions and are NOT rejected (valid benchmark criticals run ~0-4%
    # negative normals, <=20% interslice tension).
    frac_N_neg = float(np.mean(N < 0)) if n else 0.0
    # Interslice "tension" sign is convention-dependent: on right-facing slopes the
    # caller negates theta_list, which flips the sign of Z, so there tension is Z>0.
    Z_int = Z[1:n]
    z_tension = (Z_int > 0) if right_facing else (Z_int < 0)
    frac_Z_tension = float(np.mean(z_tension)) if n > 1 else 0.0
    if frac_N_neg > 0.5 or frac_Z_tension > 0.5:
        return False, (
            "force_equilibrium: inadmissible solution "
            f"({100*frac_N_neg:.0f}% of base normals in tension, "
            f"{100*frac_Z_tension:.0f}% interslice tension)")

    slice_df['n_eff'] = N  # store effective normal forces in slice_df
    slice_df['z'] = Z[:-1]  # store interslice forces in slice_df, adjust length to n slices

    # Report-only admissibility screen (corps/lowe inherit it). The parallel-force
    # march exposes no line of thrust, so only the base-tension and interslice-
    # tension signatures apply; the third is skipped. On right-facing slopes the
    # caller negates theta_list, flipping Z's sign, so tension there is Z>0 — pass
    # the physical-convention Z (tension < 0) the helper expects.
    Z_phys = -Z if right_facing else Z
    warns = _admissibility_warnings(c, N, Z_phys)

    if debug:
        r_opt = residual(FS_opt)
        print(f" Converged FS = {FS_opt:.6f}, residual = {r_opt:.4g}")

    return True, {'FS': FS_opt, 'warnings': warns}

solve_all(slice_df, rapid=False)

Executes all available limit equilibrium solution methods sequentially.

Runs six different limit equilibrium methods on the provided slice dataframe and displays the factor of safety for each method. This is useful for comparing results across multiple solution approaches.

Parameters

slice_df : pandas.DataFrame Slice dataframe containing all required columns for all methods. Must include: 'alpha', 'phi', 'c', 'w', 'u', 'dl', 'dload', 'd_x', 'd_y', 'beta', 'kw', 't', 'y_t', 'p', 'x_c', 'y_cg', and additional columns required for specific methods (e.g., 'r', 'xo', 'yo' for circular methods).

Returns

None Results are printed to console for each method.

Notes

Methods executed in order: 1. Ordinary Method of Slices (OMS) 2. Bishop's Simplified Method 3. Janbu's Simplified Method 4. Corps of Engineers Method 5. Lowe & Karafiath Method 6. Spencer's Method 7. Morgenstern-Price Method

If any method fails, an error message is displayed but execution continues with the remaining methods.

Source code in xslope/solve.py
def solve_all(slice_df, rapid=False):
    """
    Executes all available limit equilibrium solution methods sequentially.

    Runs six different limit equilibrium methods on the provided slice dataframe
    and displays the factor of safety for each method. This is useful for comparing
    results across multiple solution approaches.

    Parameters
    ----------
    slice_df : pandas.DataFrame
        Slice dataframe containing all required columns for all methods.
        Must include: 'alpha', 'phi', 'c', 'w', 'u', 'dl', 'dload', 'd_x', 'd_y',
        'beta', 'kw', 't', 'y_t', 'p', 'x_c', 'y_cg', and additional columns
        required for specific methods (e.g., 'r', 'xo', 'yo' for circular methods).

    Returns
    -------
    None
        Results are printed to console for each method.

    Notes
    -----
    Methods executed in order:
    1. Ordinary Method of Slices (OMS)
    2. Bishop's Simplified Method
    3. Janbu's Simplified Method
    4. Corps of Engineers Method
    5. Lowe & Karafiath Method
    6. Spencer's Method
    7. Morgenstern-Price Method

    If any method fails, an error message is displayed but execution continues
    with the remaining methods.
    """
    solve_selected('oms', slice_df, rapid=rapid)
    solve_selected('bishop', slice_df, rapid=rapid)
    solve_selected('janbu', slice_df, rapid=rapid)
    solve_selected('corps', slice_df, rapid=rapid)
    solve_selected('lowe', slice_df, rapid=rapid)
    solve_selected('spencer', slice_df, rapid=rapid)
    solve_selected('mprice', slice_df, rapid=rapid)

solve_selected(method_name, slice_df, rapid=False)

Executes a specified limit equilibrium solution method and displays results.

Parameters

method_name : str Name of the solution method function to call. Must be one of: 'oms', 'bishop', 'janbu', 'spencer', 'corps', 'lowe', 'mprice' slice_df : pandas.DataFrame Slice dataframe containing all required columns for the specified method (see individual method documentation for column requirements) rapid : bool, optional If True, performs rapid drawdown analysis using the specified method. Default is False.

Returns

dict or str If successful: dictionary containing method results (includes 'FS' and method-specific parameters) If failed: error message string

Notes

This function automatically prints the factor of safety and method-specific parameters to the console. For methods with additional parameters: - Spencer: displays theta (interslice force angle) - Janbu: displays fo (correction factor) - Corps of Engineers: displays theta

Source code in xslope/solve.py
def solve_selected(method_name, slice_df, rapid=False):
    """
    Executes a specified limit equilibrium solution method and displays results.

    Parameters
    ----------
    method_name : str
        Name of the solution method function to call. Must be one of:
        'oms', 'bishop', 'janbu', 'spencer', 'corps', 'lowe',
        'mprice'
    slice_df : pandas.DataFrame
        Slice dataframe containing all required columns for the specified method
        (see individual method documentation for column requirements)
    rapid : bool, optional
        If True, performs rapid drawdown analysis using the specified method.
        Default is False.

    Returns
    -------
    dict or str
        If successful: dictionary containing method results (includes 'FS' and method-specific parameters)
        If failed: error message string

    Notes
    -----
    This function automatically prints the factor of safety and method-specific
    parameters to the console. For methods with additional parameters:
    - Spencer: displays theta (interslice force angle)
    - Janbu: displays fo (correction factor)
    - Corps of Engineers: displays theta
    """

    func = globals()[method_name]

    if rapid:
        success, result = rapid_drawdown(slice_df, method_name)
    else:
        success, result = func(slice_df)
    if not success:
        print(f'Error: {result}')
        return result

    if func == oms:
        print(f'OMS: FS={result["FS"]:.3f}')
    elif func == bishop:
        print(f'Bishop: FS={result["FS"]:.3f}')
    elif func == spencer:
        print(f'Spencer: FS={result["FS"]:.3f}, theta={result["theta"]:.2f}')
        for w in result.get('warnings', []):
            print(f'  Spencer admissibility warning: {w}')
    elif func == janbu:
        print(f'Janbu Corrected FS={result["FS"]:.3f}, fo={result["fo"]:.2f}')
    elif func == corps:
        print(f'Corps Engineers: FS={result["FS"]:.3f}, theta={result["theta"]:.2f}')
        for w in result.get('warnings', []):
            print(f'  Corps admissibility warning: {w}')
    elif func == lowe:
        print(f'Lowe & Karafiath: FS={result["FS"]:.3f}')
        for w in result.get('warnings', []):
            print(f'  Lowe & Karafiath admissibility warning: {w}')
    elif func == mprice:
        print(f'Morgenstern-Price ({result["f_type"]}): FS={result["FS"]:.3f}, '
              f'lambda={result["lambda"]:.3f}')
        for w in result.get('warnings', []):
            print(f'  Morgenstern-Price admissibility warning: {w}')

    return result