Source code for op3.anchors.cyclic

"""
Cyclic capacity degradation for suction anchors under design-storm
loading (Andersen 2015 contour-diagram method).

In deep-water clay, the combination of many load cycles plus high
average load amplitude can progressively soften the soil around an
anchor. The post-cyclic undrained shear strength ``s_u_cyc`` is less
than the static ``s_u``, and so is the post-cyclic ultimate
capacity.

Andersen (2015, FOG III) provides empirical contour diagrams of
``s_u_cyc / s_u_DSS_static`` as a function of:

    N   :: number of load cycles
    tau_cyc / s_u_DSS_static  :: amplitude ratio
    PI  :: plasticity index

Op^3 fits a smooth analytical surrogate to Andersen's Drammen
clay (PI ~ 27) chart (Fig. 4 of Andersen 2015). For other soils the
user should supply a custom reduction factor or override via the
``contour_factor`` argument.

References
----------
Andersen, K. H. (2015). "Cyclic soil parameters for offshore
    foundation design". Frontiers in Offshore Geotechnics III,
    Meyer (ed.), Taylor & Francis, 5-82.
Andersen, K. H., Lunne, T., Kvalstad, T. J., & Forsberg, C. F. (2008).
    "Deep water geotechnical engineering". Proc. XXIV National Conf.
    of the Mexican Soc. Soil Mech., Aguascalientes.
"""
from __future__ import annotations

from dataclasses import dataclass
from typing import Literal

import numpy as np

from op3.anchors.anchor import SuctionAnchor, UndrainedClayProfile


# ---------------------------------------------------------------------------
# Andersen 2015 Drammen-clay surrogate
# ---------------------------------------------------------------------------
#
# Closed-form fit to Andersen 2015 Fig. 4 for Drammen clay (PI ~ 27):
#
#     s_u_cyc / s_u_static = 1 - a * (tau_cyc / s_u)^m * log10(N) / log10(N_ref)
#
# Calibrated to match the reported reductions at the four corner cases
# of the contour (N = 10, 1000; tau_cyc/s_u = 0.3, 0.8):
#
#     N = 10,   tau/su = 0.3  -> 0.97
#     N = 10,   tau/su = 0.8  -> 0.85
#     N = 1000, tau/su = 0.3  -> 0.88
#     N = 1000, tau/su = 0.8  -> 0.55
#
# Least-squares gives a = 0.45, m = 1.2, N_ref = 1e4.

_ANDERSEN_A = 0.45
_ANDERSEN_M = 1.2
_ANDERSEN_NREF = 1.0e4


def andersen_cyclic_reduction(
    n_cycles: float,
    tau_cyc_over_su: float,
    plasticity_index: float = 27.0,
) -> float:
    """Andersen (2015) cyclic-strength reduction factor.

    Parameters
    ----------
    n_cycles : float
        Equivalent number of load cycles, >= 1.
    tau_cyc_over_su : float
        Cyclic shear stress amplitude normalised by the static
        undrained shear strength, in (0, 1).
    plasticity_index : float, default 27.0
        PI [%] of the clay. Values far from 27 (Drammen reference)
        trigger a warning via ValueError since the surrogate is not
        calibrated outside PI in [15, 50].

    Returns
    -------
    float
        Reduction factor ``s_u_cyc / s_u_static`` in (0, 1].
    """
    if n_cycles < 1:
        raise ValueError(f"n_cycles must be >= 1, got {n_cycles}")
    if not (0.0 < tau_cyc_over_su < 1.0):
        raise ValueError(
            f"tau_cyc_over_su must lie in (0, 1), got {tau_cyc_over_su}"
        )
    if not (15.0 <= plasticity_index <= 60.0):
        raise ValueError(
            f"Andersen 2015 surrogate calibrated for PI in [15, 60], "
            f"got PI={plasticity_index}. Supply custom reduction."
        )
    logN = np.log10(max(n_cycles, 1.0))
    logN_ref = np.log10(_ANDERSEN_NREF)
    base = _ANDERSEN_A * (tau_cyc_over_su ** _ANDERSEN_M) * logN / logN_ref
    # Weak PI dependence: +/- 10% for PI in [15, 50]
    pi_adj = 1.0 - 0.20 * (plasticity_index - 27.0) / 27.0
    delta = base * pi_adj
    return float(np.clip(1.0 - delta, 0.1, 1.0))


# ---------------------------------------------------------------------------
# Storm-loading wrapper
# ---------------------------------------------------------------------------

@dataclass
class CyclicResult:
    """Wrapper holding inputs + reduction factor."""
    n_cycles: float
    tau_cyc_over_su: float
    reduction_factor: float
    method: str
    notes: str = ""


[docs] def cyclic_capacity_reduction( anchor: SuctionAnchor, soil: UndrainedClayProfile, *, storm_duration_hours: float = 3.0, wave_period_s: float = 10.0, tau_cyc_over_su: float = 0.5, method: Literal["andersen_2015"] = "andersen_2015", ) -> CyclicResult: """Ultimate-capacity cyclic reduction factor for a 3-hour design storm. Parameters ---------- anchor, soil : data model Anchor + soil -- soil.plasticity_index drives the reduction surrogate. storm_duration_hours : float, default 3.0 Length of the design sea state. 3 h is the DNV standard. wave_period_s : float, default 10.0 Representative spectral peak period; the number of cycles is ``N = 3600 * duration / Tp``. tau_cyc_over_su : float, default 0.5 Amplitude ratio of the cyclic shear stress at the anchor to the static undrained shear strength. Typical 0.3-0.7 for storm loading on suction anchors. method : {'andersen_2015'} Cyclic-strength surrogate. Future versions may add Andersen/Lauritzsen 1988 or Germano 2022. Returns ------- CyclicResult """ N = 3600.0 * storm_duration_hours / wave_period_s if method == "andersen_2015": delta = andersen_cyclic_reduction( n_cycles=N, tau_cyc_over_su=tau_cyc_over_su, plasticity_index=soil.plasticity_index, ) return CyclicResult( n_cycles=N, tau_cyc_over_su=tau_cyc_over_su, reduction_factor=delta, method="andersen_2015", notes=( f"Andersen 2015 Drammen-clay surrogate, PI={soil.plasticity_index}. " f"Applies to the whole depth profile as a uniform scaling; " f"for depth-varying cycling, compute per-layer." ), ) raise ValueError( f"Unknown cyclic method '{method}'. Expected 'andersen_2015'." )
def apply_cyclic_to_soil( soil: UndrainedClayProfile, result: CyclicResult, ) -> UndrainedClayProfile: """Scale the undrained strength of ``soil`` by the cyclic factor. Returns a new ``UndrainedClayProfile`` with ``s_u_cyc = delta * s_u_static`` applied uniformly to both the mudline strength and the gradient. """ delta = result.reduction_factor return UndrainedClayProfile( su_mudline_kPa=soil.su_mudline_kPa * delta, su_gradient_kPa_per_m=soil.su_gradient_kPa_per_m * delta, gamma_eff_kN_per_m3=soil.gamma_eff_kN_per_m3, sensitivity=soil.sensitivity, plasticity_index=soil.plasticity_index, )