from __future__ import annotations
from typing import Any, Dict, List, Tuple
import numpy as np
from matplotlib import pyplot as plt
from matplotlib.axes import Axes
from numpy.typing import NDArray
from pypilecore.common.piles.geometry.components import (
RectPileGeometryComponent,
RoundPileGeometryComponent,
)
from pypilecore.common.piles.geometry.materials import Color, PileMaterial
[docs]class PileGeometry:
"""The PileGeometry class represents the geometry of a pile."""
def __init__(
self,
components: List[RoundPileGeometryComponent | RectPileGeometryComponent],
materials: List[PileMaterial] | None = None,
pile_tip_factor_s: float | None = None,
beta_p: float | None = None,
):
"""
Represents the geometry of a pile.
Parameters:
-----------
components : list
A list of pile geometry components.
materials : list, optional
A list of materials used in the pile geometry, by default None.
pile_tip_factor_s : float, optional
The pile tip factor S, by default None.
beta_p : float, optional
The beta_p value, by default None.
"""
self._components = components
self._materials = materials
self._pile_tip_factor_s = pile_tip_factor_s
self._beta_p = beta_p
[docs] @classmethod
def from_api_response(cls, geometry: dict) -> PileGeometry:
"""
Instantiates a PileGeometry from a geometry object in the API response payload.
Parameters:
-----------
geometry: dict
A dictionary that is retrieved from the API response payload at "/pile_properties/geometry".
Returns:
--------
PileGeometry
A pile geometry.
"""
components: List[RoundPileGeometryComponent | RectPileGeometryComponent] = []
for component in geometry["components"]:
if component["outer_shape"] == "round":
components.append(
RoundPileGeometryComponent.from_api_response(component)
)
else:
components.append(
RectPileGeometryComponent.from_api_response(component)
)
materials = []
if "materials" in geometry:
for material in geometry["materials"]:
materials.append(PileMaterial.from_api_response(material))
return cls(
components=components,
materials=materials,
pile_tip_factor_s=geometry["properties"].get("pile_tip_factor_s"),
beta_p=geometry["properties"]["beta_p"],
)
@property
def components(
self,
) -> List[RoundPileGeometryComponent | RectPileGeometryComponent]:
"""The components of the pile geometry"""
return self._components
@property
def materials(self) -> List[PileMaterial]:
"""The materials used in the pile geometry"""
return self._materials if self._materials is not None else []
@property
def materials_dict(self) -> Dict[str, PileMaterial]:
"""The materials used in the pile geometry as a dictionary with the material name as key"""
return {material.name: material for material in self.materials}
@property
def pile_tip_factor_s(self) -> float | None:
"""The pile tip factor S of the pile geometry"""
return self._pile_tip_factor_s
@property
def beta_p(self) -> float | None:
"""The beta_p value of the pile geometry"""
return self._beta_p
@property
def equiv_diameter_pile_tip(self) -> float:
"""The equivalent diameter of the pile at the pile tip."""
return self.components[-1].equiv_tip_diameter
@property
def circumference_pile_tip(self) -> float:
"""The outer-circumference of the pile at the pile tip."""
return self.components[-1].circumference
@property
def area_pile_tip(self) -> float:
"""The area of the pile at the pile tip."""
return self.components[-1].area_full
[docs] def serialize_payload(self) -> Dict[str, list | dict]:
"""
Serialize the pile geometry to a dictionary payload for the API.
Returns:
A dictionary payload containing the components, materials (if set), pile tip factor S (if set), and beta_p (if set).
"""
components = [component.serialize_payload() for component in self.components]
payload: Dict[str, Any] = {"components": components}
if self.materials is not None and len(self.materials) > 0:
materials = [material.serialize_payload() for material in self.materials]
payload["materials"] = materials
custom_geom_properties: Dict[str, float] = {}
if self.pile_tip_factor_s is not None:
custom_geom_properties["pile_tip_factor_s"] = self.pile_tip_factor_s
if self.beta_p is not None:
custom_geom_properties["beta_p"] = self.beta_p
if len(custom_geom_properties.keys()) > 0:
payload["custom_properties"] = custom_geom_properties
return payload
[docs] def get_circum_vs_depth(
self,
pile_tip_level_nap: float | int,
pile_head_level_nap: float | int,
depth_nap: NDArray[np.floating],
) -> NDArray[np.floating]:
"""
Returns pile circumferences at requested depths.
Parameters
---------_
pile_tip_level_nap : float
Pile tip level in [m] w.r.t. NAP.
pile_head_level_nap : float
Pile head level in [m] w.r.t. NAP.
depth_nap : np.array
Array with depths in [m] w.r.t. NAP.
Returns
-------
np.array
Array with pile circumferences at the requested `depth_nap` levels.
"""
circum_vs_depth = np.zeros_like(depth_nap)
# Use the maximum circumference of all components at each depth.
for component in self.components:
circum_vs_depth = np.maximum(
circum_vs_depth,
component.get_circum_vs_depth(
pile_tip_level_nap=pile_tip_level_nap,
pile_head_level_nap=pile_head_level_nap,
depth_nap=depth_nap,
),
)
return circum_vs_depth
[docs] def get_area_vs_depth(
self,
pile_tip_level_nap: float | int,
pile_head_level_nap: float | int,
depth_nap: NDArray[np.floating],
) -> NDArray[np.floating]:
"""
Returns cross-sectional area of the pile at requested depths.
Parameters
---------_
pile_tip_level_nap : float
Pile tip level in [m] w.r.t. NAP.
pile_head_level_nap : float
Pile head level in [m] w.r.t. NAP.
depth_nap : np.array
Array with depths in [m] w.r.t. NAP.
Returns
-------
np.array
Array with pile areas at the requested `depth_nap` levels.
"""
area_vs_depth = np.zeros_like(depth_nap)
# Use the maximum area of all components at each depth.
for component in self.components:
area_vs_depth = np.maximum(
area_vs_depth,
component.get_area_vs_depth(
pile_tip_level_nap=pile_tip_level_nap,
pile_head_level_nap=pile_head_level_nap,
depth_nap=depth_nap,
),
)
return area_vs_depth
[docs] def plot(
self,
pile_tip_level_nap: float | int = -10,
pile_head_level_nap: float | int = 0,
figsize: Tuple[float, float] | None = (3, 9),
show: bool = True,
**kwargs: Any,
) -> List[Axes]:
"""
Plot the top-view of the pile at a specified depth.
Parameters
----------
pile_tip_level_nap : float, optional
The pile tip level in m w.r.t. NAP, by default -10.
pile_head_level_nap : float, optional
The pile head level in m w.r.t. NAP, by default 0.
figsize : tuple, optional
The figure size (width, height) in inches, by default (6.0, 6.0).
show : bool, optional
Whether to display the plot, by default True.
**kwargs
Additional keyword arguments to pass to the `plt
"""
kwargs_subplot = {
"figsize": figsize,
"gridspec_kw": {
"hspace": 0.15,
"height_ratios": [1, 4],
},
}
kwargs_subplot.update(kwargs)
height_ratio = (
kwargs_subplot["gridspec_kw"]["height_ratios"][1] # type: ignore
/ kwargs_subplot["gridspec_kw"]["height_ratios"][0] # type: ignore
)
_, axes = plt.subplots(
2,
1,
**kwargs_subplot,
)
x_ticks = set([0.0])
y_ticks = set([0.0])
for component in self.components[::-1]:
facecolor = "grey"
if component.material in self.materials_dict:
material_color = self.materials_dict[component.material].color
if isinstance(material_color, Color):
facecolor = material_color.hex
elif isinstance(material_color, Color):
facecolor = (
material_color.red,
material_color.green,
material_color.blue,
)
component.plot_cross_section_exterior(
axes=axes[0],
facecolor=facecolor,
edgecolor="black",
axis_arg=None,
show=False,
)
component.plot_side_view(
pile_tip_level_nap=pile_tip_level_nap,
pile_head_level_nap=pile_head_level_nap,
axes=axes[1],
facecolor=facecolor,
axis_arg=None,
show=False,
)
x_ticks.add(component.cross_section_bounds[0])
x_ticks.add(component.cross_section_bounds[1])
y_ticks.add(component.cross_section_bounds[2])
y_ticks.add(component.cross_section_bounds[3])
axes[0].axis("scaled")
axes[0].set(aspect=1)
axes[1].axis("auto")
ax1_aspect = (
abs(axes[0].axis()[2] - axes[0].axis()[3])
/ abs(axes[1].axis()[2] - axes[1].axis()[3])
* height_ratio
)
axes[1].set(aspect=ax1_aspect)
axes[0].spines[:].set_visible(False)
axes[1].spines[:].set_visible(False)
axes[0].set_xticks(ticks=list(x_ticks))
axes[0].set_yticks(ticks=list(y_ticks))
axes[1].set_xticks(ticks=list(x_ticks))
axes[1].set_yticks(
ticks=[pile_head_level_nap, pile_tip_level_nap],
labels=["Pile Head", "Pile Tip"],
)
axes[0].tick_params(axis="x", labelrotation=45)
axes[1].tick_params(axis="x", labelrotation=45)
if show:
plt.show()
return axes