Source code for op3.anchors.anchor

"""
Data model for suction-anchor analysis.

Three dataclasses, all SI-consistent with the rest of Op^3:

    SuctionAnchor           geometry + steel properties
    UndrainedClayProfile    linearly increasing undrained shear strength
    MooringLoad             tension + angle at padeye

References
----------
DNV-RP-E303 (2021), Section 2.2 "Geometry and notation".
Randolph, M. F., & Gourvenec, S. (2011). Offshore Geotechnical
    Engineering, Ch. 9.
Aubeny, C. P., Han, S.-W., & Murff, J. D. (2003). "Inclined load
    capacity of suction caissons". IJNAMG 27(14), 1235-1254.
"""
from __future__ import annotations

from dataclasses import dataclass, field
from typing import Optional

import numpy as np


# ---------------------------------------------------------------------------
# Geometry
# ---------------------------------------------------------------------------

[docs] @dataclass class SuctionAnchor: """Suction-anchor geometry and steel properties. All dimensions in SI units (m, mm for wall thickness, kN for weight). Parameters ---------- diameter_m : float Outer diameter ``D``. Practical range 3-8 m. skirt_length_m : float Embedded skirt length ``L``. Typical range 10-30 m in deep-water clay; aspect ratio ``L/D`` usually between 1 and 6. wall_thickness_mm : float, default 30.0 Skirt wall thickness. Typical range 20-50 mm. padeye_depth_m : Optional[float] Depth of the mooring attachment point below the mudline. If ``None``, the caller must set it (``optimal_padeye_*`` helpers in :mod:`op3.anchors.padeye` return a recommendation). padeye_offset_m : float, default 0.0 Lateral offset of the padeye from the anchor centreline. A non-zero value introduces an installation-phase torque in some models but is ignored by the capacity methods implemented here (they assume plane of symmetry). lid_thickness_mm : float, default 40.0 Top-cap plate thickness. submerged_weight_kN : float, default 0.0 Total submerged weight of the anchor (steel minus buoyancy of displaced water). Used in self-weight penetration and vertical capacity. ``0.0`` is the conservative default for design-stage capacity checks. steel_grade : str, default "S355" Informational only; not used by any calculation. Notes ----- The "outer" diameter is used throughout for shaft friction and lid area. The inner diameter is derived as ``D - 2 * wall_thickness``. Wall thickness enters only via inner-shaft and annulus areas and is a second-order effect for typical offshore anchors. """ diameter_m: float skirt_length_m: float wall_thickness_mm: float = 30.0 padeye_depth_m: Optional[float] = None padeye_offset_m: float = 0.0 lid_thickness_mm: float = 40.0 submerged_weight_kN: float = 0.0 steel_grade: str = "S355" def __post_init__(self) -> None: if self.diameter_m <= 0: raise ValueError(f"diameter_m must be > 0, got {self.diameter_m}") if self.skirt_length_m <= 0: raise ValueError(f"skirt_length_m must be > 0, got {self.skirt_length_m}") if self.wall_thickness_mm <= 0: raise ValueError( f"wall_thickness_mm must be > 0, got {self.wall_thickness_mm}" ) if self.padeye_depth_m is not None: if not (0.0 < self.padeye_depth_m < self.skirt_length_m): raise ValueError( f"padeye_depth_m must satisfy 0 < z_p < L " f"(L={self.skirt_length_m}), got {self.padeye_depth_m}" ) # Physical plausibility: wall thickness must be smaller than radius if self.wall_thickness_mm / 1000.0 >= self.diameter_m / 2.0: raise ValueError( f"wall_thickness_mm={self.wall_thickness_mm} mm exceeds " f"radius={self.diameter_m / 2.0 * 1000:.1f} mm" ) @property def aspect_ratio(self) -> float: """L/D ratio (dimensionless).""" return self.skirt_length_m / self.diameter_m @property def inner_diameter_m(self) -> float: """Inner diameter D - 2*t_w.""" return self.diameter_m - 2.0 * self.wall_thickness_mm / 1000.0 @property def outer_skirt_area_m2(self) -> float: """Outer shaft surface area in contact with soil, ``pi * D * L``.""" return np.pi * self.diameter_m * self.skirt_length_m @property def inner_skirt_area_m2(self) -> float: """Inner shaft surface area in contact with soil plug, ``pi * D_i * L``.""" return np.pi * self.inner_diameter_m * self.skirt_length_m @property def lid_area_m2(self) -> float: """Full plan area of the top cap, ``pi/4 * D**2``.""" return np.pi / 4.0 * self.diameter_m ** 2 @property def lid_inner_area_m2(self) -> float: """Interior plan area (soil-plug face area), ``pi/4 * D_i**2``.""" return np.pi / 4.0 * self.inner_diameter_m ** 2 @property def annulus_area_m2(self) -> float: """Steel annulus area at skirt tip, ``pi/4 * (D^2 - D_i^2)``.""" return np.pi / 4.0 * (self.diameter_m ** 2 - self.inner_diameter_m ** 2)
# --------------------------------------------------------------------------- # Soil # ---------------------------------------------------------------------------
[docs] @dataclass class UndrainedClayProfile: """Linearly increasing undrained shear strength profile. This is the standard design profile for deep-water normally or lightly over-consolidated clay at anchor sites. For more complex profiles, split into layers and integrate numerically. Parameters ---------- su_mudline_kPa : float ``s_u(z=0)``. Typical range 2-10 kPa for NC clay, higher for over-consolidated crusts. su_gradient_kPa_per_m : float ``k = ds_u/dz``. Typical range 1-3 kPa/m. gamma_eff_kN_per_m3 : float, default 6.0 Effective unit weight of the clay (saturated minus seawater). 6 kN/m^3 is typical for Gulf-of-Mexico soft clay per API RP 2GEO Table 8.5. sensitivity : float, default 3.0 ``S_t = s_u / s_u_remoulded``. Drives self-weight penetration resistance. Typical 2-5. plasticity_index : float, default 30.0 ``PI`` [%]. Drives cyclic strength per Andersen 2015 contour diagrams. References ---------- Randolph & Gourvenec 2011, Ch. 3. API RP 2GEO (2011) Table 8.5. """ su_mudline_kPa: float su_gradient_kPa_per_m: float gamma_eff_kN_per_m3: float = 6.0 sensitivity: float = 3.0 plasticity_index: float = 30.0 def __post_init__(self) -> None: if self.su_mudline_kPa < 0: raise ValueError( f"su_mudline_kPa must be >= 0, got {self.su_mudline_kPa}" ) if self.su_gradient_kPa_per_m < 0: raise ValueError( f"su_gradient_kPa_per_m must be >= 0, got {self.su_gradient_kPa_per_m}" ) if self.sensitivity <= 0: raise ValueError( f"sensitivity must be > 0, got {self.sensitivity}" ) if self.gamma_eff_kN_per_m3 <= 0: raise ValueError( f"gamma_eff_kN_per_m3 must be > 0, got {self.gamma_eff_kN_per_m3}" )
[docs] def su_at_depth(self, z_m: float | np.ndarray) -> float | np.ndarray: """Intact undrained shear strength at depth z below mudline.""" return self.su_mudline_kPa + self.su_gradient_kPa_per_m * np.asarray(z_m)
[docs] def su_remoulded_at_depth( self, z_m: float | np.ndarray ) -> float | np.ndarray: """Remoulded undrained shear strength, ``s_u / S_t``.""" return self.su_at_depth(z_m) / self.sensitivity
[docs] def su_average_to_depth(self, z_m: float) -> float: """Depth-averaged intact s_u from mudline to z. For a linear profile this is ``s_u_mudline + k * z / 2``. """ if z_m <= 0: return self.su_mudline_kPa return self.su_mudline_kPa + 0.5 * self.su_gradient_kPa_per_m * z_m
# --------------------------------------------------------------------------- # Mooring load at padeye # ---------------------------------------------------------------------------
[docs] @dataclass class MooringLoad: """Mooring-line load vector at the padeye. The angle is measured from the horizontal; ``angle_at_padeye_deg`` accounts for inverse-catenary rotation from the fairlead-side angle. Parameters ---------- tension_kN : float Line tension magnitude ``T``. angle_at_padeye_deg : float Angle of the tension vector from horizontal at the padeye, after the inverse-catenary solution. Range 0 (pure horizontal pull) to 90 (pure uplift). """ tension_kN: float angle_at_padeye_deg: float def __post_init__(self) -> None: if self.tension_kN < 0: raise ValueError(f"tension_kN must be >= 0, got {self.tension_kN}") if not (-90.0 <= self.angle_at_padeye_deg <= 90.0): raise ValueError( f"angle_at_padeye_deg must lie in [-90, 90], " f"got {self.angle_at_padeye_deg}" ) @property def horizontal_kN(self) -> float: """Horizontal component, ``T * cos(theta)``.""" return self.tension_kN * np.cos(np.radians(self.angle_at_padeye_deg)) @property def vertical_kN(self) -> float: """Vertical (uplift) component, ``T * sin(theta)``. Positive = up.""" return self.tension_kN * np.sin(np.radians(self.angle_at_padeye_deg))