Skip to content

ge.profile — API reference

Soil dataclass

Python
Soil(name: str = 'Soil', gamma: float = 18.0, gamma_sat: Optional[float] = None, phi: float = 30.0, c: float = 0.0, e: Optional[float] = None, Gs: float = 2.7, k: Optional[float] = None, Es: Optional[float] = None, mu: float = 0.3, Cc: Optional[float] = None, Cr: Optional[float] = None, OCR: float = 1.0, Su: Optional[float] = None, description: str = '')

A single soil layer's engineering properties.

Examples:

Python Console Session
>>> from geoeq.profile import Soil
>>> clay = Soil("Soft Clay", gamma=17, gamma_sat=18.5, phi=0, c=25, e=0.9)
>>> clay.name
'Soft Clay'

gamma_effective

Python
gamma_effective() -> float

Submerged (buoyant) unit weight gamma' = gamma_sat - gamma_w.

Source code in geoeq/profile/soil.py
Python
def gamma_effective(self) -> float:
    """Submerged (buoyant) unit weight gamma' = gamma_sat - gamma_w."""
    from geoeq.core.constants import GAMMA_WATER
    return float(self.gamma_sat) - GAMMA_WATER

SoilProfile

Python
SoilProfile(layers: Sequence[LayerInput], water_table: Optional[float] = None)

Layered soil profile with stress computations and plotting.

PARAMETER DESCRIPTION
layers

Layers in order of increasing depth. Layers must be contiguous and non-overlapping. Tops and bottoms in metres.

TYPE: sequence of (top, bot, Soil)

water_table

Depth of the phreatic surface (m, positive downward). None or np.inf means no water table (dry profile).

TYPE: float DEFAULT: None

Examples:

Python Console Session
>>> from geoeq.profile import Soil, SoilProfile
>>> p = SoilProfile([
...     (0, 2, Soil("Fill",       gamma=18)),
...     (2, 8, Soil("Soft Clay",  gamma=17, gamma_sat=18.5)),
...     (8, 20, Soil("Dense Sand", gamma=19, gamma_sat=20.5)),
... ], water_table=2.0)
>>> round(p.effective_stress(10), 1)
133.7
Source code in geoeq/profile/profile.py
Python
def __init__(
    self,
    layers: Sequence[LayerInput],
    water_table: Optional[float] = None,
):
    if not layers:
        raise ValueError("SoilProfile needs at least one layer.")
    self._layers: List[_Layer] = []
    prev_bot = layers[0][0]
    for i, (top, bot, soil) in enumerate(layers):
        if bot <= top:
            raise ValueError(
                f"Layer {i}: bottom ({bot}) must be > top ({top}).")
        if not np.isclose(top, prev_bot):
            raise ValueError(
                f"Layer {i} starts at {top} m but previous layer ended "
                f"at {prev_bot} m -- profile must be contiguous.")
        if not isinstance(soil, Soil):
            raise TypeError(
                f"Layer {i}: third element must be a Soil instance.")
        self._layers.append(_Layer(float(top), float(bot), soil))
        prev_bot = bot
    self.water_table = (
        float("inf") if water_table is None else float(water_table))

layers

Python
layers() -> List[Tuple[float, float, Soil]]

Return layers as a list of (top, bot, Soil) tuples.

Source code in geoeq/profile/profile.py
Python
def layers(self) -> List[Tuple[float, float, Soil]]:
    """Return layers as a list of ``(top, bot, Soil)`` tuples."""
    return [(L.top, L.bot, L.soil) for L in self._layers]

layer_at

Python
layer_at(z: float) -> Soil

Return the Soil instance at depth z (m).

Source code in geoeq/profile/profile.py
Python
def layer_at(self, z: float) -> Soil:
    """Return the ``Soil`` instance at depth z (m)."""
    z = float(z)
    if z < self.top or z > self.bottom:
        raise ValueError(
            f"Depth {z} m is outside the profile "
            f"({self.top}..{self.bottom}).")
    for L in self._layers:
        if L.top <= z <= L.bot:
            return L.soil
    raise ValueError(f"Depth {z} m not found in any layer.")

add_layer

Python
add_layer(top: float, bot: float, soil: Soil) -> None

Append a layer at the bottom of the profile.

Source code in geoeq/profile/profile.py
Python
def add_layer(self, top: float, bot: float, soil: Soil) -> None:
    """Append a layer at the bottom of the profile."""
    if not np.isclose(top, self.bottom):
        raise ValueError(
            f"New layer must start at current bottom "
            f"({self.bottom} m), got {top} m.")
    if bot <= top:
        raise ValueError(f"bot ({bot}) must be > top ({top}).")
    self._layers.append(_Layer(float(top), float(bot), soil))

total_stress

Python
total_stress(z: Union[float, Iterable[float]]) -> Union[float, np.ndarray]

Total vertical stress sigma_v at depth z (kPa).

Notes

Integrates gamma above water table and gamma_sat below. If the water table is above the ground surface, hydrostatic pressure of the standing water is added.

Source code in geoeq/profile/profile.py
Python
def total_stress(self, z: Union[float, Iterable[float]]) -> Union[float, np.ndarray]:
    """Total vertical stress sigma_v at depth z (kPa).

    Notes
    -----
    Integrates ``gamma`` above water table and ``gamma_sat`` below.
    If the water table is above the ground surface, hydrostatic
    pressure of the standing water is added.
    """
    z_arr = np.atleast_1d(np.asarray(z, dtype=float))
    out = np.zeros_like(z_arr)
    for i, zi in enumerate(z_arr):
        out[i] = self._sigma_at(float(zi))
    return float(out[0]) if np.isscalar(z) else out

pore_pressure

Python
pore_pressure(z: Union[float, Iterable[float]]) -> Union[float, np.ndarray]

Hydrostatic pore water pressure u at depth z (kPa).

Source code in geoeq/profile/profile.py
Python
def pore_pressure(
    self, z: Union[float, Iterable[float]]
) -> Union[float, np.ndarray]:
    """Hydrostatic pore water pressure u at depth z (kPa)."""
    z_arr = np.atleast_1d(np.asarray(z, dtype=float))
    u = GAMMA_WATER * np.maximum(0.0, z_arr - self.water_table)
    return float(u[0]) if np.isscalar(z) else u

effective_stress

Python
effective_stress(z: Union[float, Iterable[float]]) -> Union[float, np.ndarray]

Effective vertical stress sigma'_v = sigma_v - u (kPa).

Source code in geoeq/profile/profile.py
Python
def effective_stress(
    self, z: Union[float, Iterable[float]]
) -> Union[float, np.ndarray]:
    """Effective vertical stress sigma'_v = sigma_v - u (kPa)."""
    return self.total_stress(z) - self.pore_pressure(z)

stress_at

Python
stress_at(z: float) -> dict

Return {'sigma': ..., 'u': ..., 'sigma_eff': ...} at depth z.

Source code in geoeq/profile/profile.py
Python
def stress_at(self, z: float) -> dict:
    """Return ``{'sigma': ..., 'u': ..., 'sigma_eff': ...}`` at depth z."""
    return {
        "sigma": float(self.total_stress(z)),
        "u": float(self.pore_pressure(z)),
        "sigma_eff": float(self.effective_stress(z)),
    }

to_dataframe

Python
to_dataframe()

Export layer data to a pandas DataFrame (if pandas is installed).

Source code in geoeq/profile/profile.py
Python
def to_dataframe(self):
    """Export layer data to a pandas DataFrame (if pandas is installed)."""
    try:
        import pandas as pd
    except ImportError as e:  # pragma: no cover -- soft dep
        raise ImportError("pandas is required for to_dataframe()") from e
    rows = []
    for L in self._layers:
        rows.append({
            "name": L.soil.name,
            "top": L.top,
            "bot": L.bot,
            "thickness": L.thickness,
            "gamma": L.soil.gamma,
            "gamma_sat": L.soil.gamma_sat,
            "phi": L.soil.phi,
            "c": L.soil.c,
        })
    return pd.DataFrame(rows)

plot

Python
plot(dz: float = 0.1, ax=None, show: bool = False, save_as=None)

Plot sigma, u, sigma' vs depth.

Returns the Matplotlib figure for further customization.

Source code in geoeq/profile/profile.py
Python
def plot(self, dz: float = 0.1, ax=None, show: bool = False, save_as=None):
    """Plot sigma, u, sigma' vs depth.

    Returns the Matplotlib figure for further customization.
    """
    import matplotlib.pyplot as plt
    depths = np.arange(self.top, self.bottom + dz / 2, dz)
    sigma = self.total_stress(depths)
    u = self.pore_pressure(depths)
    sigma_eff = self.effective_stress(depths)

    if ax is None:
        fig, ax = plt.subplots(figsize=(6, 8))
    else:
        fig = ax.figure
    ax.plot(sigma, depths, label=r"$\sigma$ (total)",
            color="#1f3a93", linewidth=1.8)
    ax.plot(u, depths, label=r"$u$ (pore water)",
            color="#2980b9", linewidth=1.8, linestyle="--")
    ax.plot(sigma_eff, depths, label=r"$\sigma'$ (effective)",
            color="#c0392b", linewidth=1.8)

    # Water table line.
    if np.isfinite(self.water_table):
        ax.axhline(self.water_table, color="#3498db",
                   linewidth=1.0, linestyle=":")
        ax.text(0.02, self.water_table, "  WT",
                transform=ax.get_yaxis_transform(),
                va="center", color="#2980b9", fontsize=9)
    # Layer boundaries.
    for L in self._layers[1:]:
        ax.axhline(L.top, color="0.7", linewidth=0.6)

    ax.invert_yaxis()
    ax.set_xlabel("Stress (kPa)")
    ax.set_ylabel("Depth (m)")
    ax.set_title("Stress profile")
    ax.grid(True, alpha=0.3)
    ax.legend(loc="lower right")

    if save_as:
        fig.savefig(save_as, dpi=300, bbox_inches="tight")
    if show:  # pragma: no cover -- interactive
        plt.show()
    return fig

mesh

Python
mesh(profile: SoilProfile, dz: float = 0.5) -> np.ndarray

Calculation grid of depths from profile top to bottom at spacing dz.

Source code in geoeq/profile/profile.py
Python
def mesh(profile: SoilProfile, dz: float = 0.5) -> np.ndarray:
    """Calculation grid of depths from profile top to bottom at spacing dz."""
    if dz <= 0:
        raise ValueError("dz must be positive.")
    return np.arange(profile.top, profile.bottom + dz / 2, dz)

log_plot

Python
log_plot(boreholes, save_as=None)

Multi-borehole log plot.

PARAMETER DESCRIPTION
boreholes

Mapping of borehole label to profile.

TYPE: dict[str, SoilProfile]

RETURNS DESCRIPTION
Figure
Source code in geoeq/profile/profile.py
Python
def log_plot(boreholes, save_as=None):  # pragma: no cover -- light stub
    """Multi-borehole log plot.

    Parameters
    ----------
    boreholes : dict[str, SoilProfile]
        Mapping of borehole label to profile.

    Returns
    -------
    matplotlib.figure.Figure
    """
    import matplotlib.pyplot as plt
    fig, axes = plt.subplots(1, len(boreholes), figsize=(3 * len(boreholes), 8),
                             sharey=True)
    if len(boreholes) == 1:
        axes = [axes]
    for ax, (name, profile) in zip(axes, boreholes.items()):
        for top, bot, soil in profile:
            ax.fill_betweenx([top, bot], 0, 1, alpha=0.4,
                             label=soil.name)
            ax.text(0.5, (top + bot) / 2, soil.name,
                    ha="center", va="center", fontsize=8)
        ax.set_xlim(0, 1)
        ax.set_xticks([])
        ax.set_title(name)
        ax.invert_yaxis()
    axes[0].set_ylabel("Depth (m)")
    if save_as:
        fig.savefig(save_as, dpi=300, bbox_inches="tight")
    return fig