"""
Foundation-type protocol and base class (new-API v1.1+).
A :class:`FoundationProtocol` implementation owns the TOPOLOGY of a
foundation (monopile, tripod, jacket, suction bucket, ...) and
delegates SOIL-STRUCTURE-INTERACTION FIDELITY to an ``SSIProtocol``
strategy from :mod:`op3.ssi`.
The split separates two concerns that v1.0 op³ conflated into a
single ``FoundationMode`` enum:
- **Type** — geometry, mass, wall schedule, bucket count, brace
layout. These are properties of the physical foundation; they
cannot be swapped at runtime without rebuilding the model.
- **SSI fidelity** — fixed base, lumped 6x6 stiffness, lumped BNWF,
physical distributed BNWF, Craig-Bampton reduction. These describe
HOW the soil-foundation interaction is represented numerically;
they are strategies the type composes with.
Every concrete type implements :class:`FoundationProtocol` and may
inherit from :class:`BaseFoundation` for the back-compat bridge to
the legacy ``Foundation`` dataclass + ``composer.compose_tower_model``
pipeline.
Validation protocol
-------------------
A concrete type is expected to live under :mod:`op3.models.<name>/`
and to maintain a ``vvc.yaml`` dossier (V&V&C: verification,
validation, calibration). Until every V&V metric in the dossier is
GREEN the type MUST NOT be re-exported from any public namespace
outside its own ``models`` directory.
"""
from __future__ import annotations
from abc import ABC, abstractmethod
from enum import Enum
from typing import Protocol, TYPE_CHECKING, runtime_checkable
import numpy as np
if TYPE_CHECKING: # pragma: no cover
from op3.foundations._legacy import Foundation
from op3.ssi.base import SSIProtocol
[docs]
class FoundationType(str, Enum):
"""High-level classification of supported foundation topologies.
These are DESCRIPTIVE labels attached to each concrete type; they
do NOT replace the legacy ``FoundationMode`` enum (which describes
SSI fidelity, not topology). A single ``FoundationType`` value can
be instantiated with any compatible ``SSIProtocol``.
"""
MONOPILE = "monopile"
TRIPOD = "tripod"
JACKET = "jacket"
SUCTION_BUCKET = "suction_bucket"
GBS = "gbs" # Gravity-based structure, placeholder for future work
[docs]
@runtime_checkable
class FoundationProtocol(Protocol):
"""Contract for a foundation TYPE.
Implementations own geometry + mass + the OpenSees topology they
need to build, and compose with an ``SSIProtocol`` strategy for
soil-structure interaction. The protocol is deliberately small so
new types (e.g. gravity-based structures) can be added without
touching the core pipeline.
"""
#: Canonical short name, e.g. ``"monopile"`` or ``"tripod"``. Used
#: for logging, dossier lookup, and registry keys.
type_name: str
#: High-level classification (see :class:`FoundationType`).
foundation_type: FoundationType
[docs]
def head_stiffness_6x6(self) -> np.ndarray:
"""Return the 6x6 stiffness at the tower-base (foundation-top)
interface, in SI units (N/m, N·m/rad, mixed off-diagonals).
For fixed-base and elastic-head-spring SSI strategies this is
analytical. For physical BNWF with Craig-Bampton reduction it
may trigger an OpenSees model build and matrix condensation.
"""
...
[docs]
def as_legacy_foundation(self) -> "Foundation":
"""Back-compat bridge: return a legacy :class:`Foundation`
(``mode=STIFFNESS_6X6``, ``stiffness_matrix=head_stiffness_6x6()``)
so the v1.0 :func:`op3.composer.compose_tower_model` pipeline
works unchanged.
Future types with non-trivial topology (skirt, braces, etc.)
will grow a second bridge, ``build_opensees(ops, base_node)``,
that instantiates the real topology directly. For v1.1 the
condensed-6x6 bridge is the only supported handover.
"""
...
[docs]
class BaseFoundation(ABC):
"""Optional base class with a default back-compat bridge.
Concrete types may subclass :class:`BaseFoundation` for the
:meth:`as_legacy_foundation` default (which delegates to
:meth:`head_stiffness_6x6`), or implement :class:`FoundationProtocol`
directly via duck typing.
Subclasses MUST:
- set ``type_name`` and ``foundation_type`` class attributes
- implement :meth:`head_stiffness_6x6`
Subclasses SHOULD:
- provide a classmethod ``.from_dossier(path)`` that loads
``site.yaml``, ``geometry.yaml``, ``soil.yaml`` from a
:mod:`op3.models` subdirectory and returns an instance
- carry a ``ssi`` attribute set by :meth:`with_ssi` so the same
topology can be re-used across fidelity levels
"""
type_name: str = "<abstract>"
foundation_type: FoundationType = FoundationType.MONOPILE # overridden
# The SSI strategy is injected post-construction via ``with_ssi``.
# It is the means by which head_stiffness_6x6 is computed.
ssi: "SSIProtocol | None" = None
[docs]
@abstractmethod
def head_stiffness_6x6(self) -> np.ndarray:
"""Compute or return the 6x6 interface stiffness.
Concrete types typically delegate to ``self.ssi.compute_head_stiffness(self)``.
"""
...
[docs]
def with_ssi(self, ssi: "SSIProtocol") -> "BaseFoundation":
"""Attach an SSI strategy and return ``self`` for chaining.
The strategy is stored on the instance; repeated calls replace
the previous strategy. The same type instance can therefore be
evaluated under multiple fidelity levels by calling
:meth:`with_ssi` then :meth:`head_stiffness_6x6` in sequence.
"""
self.ssi = ssi
return self
[docs]
def as_legacy_foundation(self) -> "Foundation":
"""Default back-compat bridge: wrap :meth:`head_stiffness_6x6`
in a legacy ``Foundation(mode=STIFFNESS_6X6)`` object.
The ``_suppress_deprecation_warning`` kwarg on
:func:`build_foundation` is intentionally NOT used here
because ``Foundation(...)`` is constructed directly (no
factory call). The legacy enum itself is silent until called.
"""
from op3.foundations._legacy import Foundation, FoundationMode
K = np.asarray(self.head_stiffness_6x6(), dtype=float)
if K.shape != (6, 6):
raise ValueError(
f"{self.type_name}.head_stiffness_6x6() returned shape "
f"{K.shape}; must be (6, 6)"
)
f = Foundation(mode=FoundationMode.STIFFNESS_6X6)
f.stiffness_matrix = K
ssi_name = getattr(self.ssi, "name", None) or "unknown-SSI"
f.source = f"{self.type_name} via as_legacy_foundation() ({ssi_name})"
return f
def __repr__(self) -> str: # pragma: no cover - cosmetic
ssi_name = getattr(self.ssi, "name", None) or "no SSI"
return f"<{type(self).__name__} type={self.type_name} ssi={ssi_name}>"