Source code for pypilecore.results.multi_cpt_results

from __future__ import annotations

from functools import lru_cache
from typing import Any, Dict, List, Sequence, Tuple, Union

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from matplotlib.axes import Axes
from matplotlib.patches import Patch

from pypilecore.common.piles import PileProperties
from pypilecore.exceptions import UserError
from pypilecore.results.load_settlement import get_load_settlement_plot
from pypilecore.results.single_cpt_results import SingleCPTBearingResults

Number = Union[float, int]


[docs]class CPTGroupResultsTable: """ Dataclass that contains the bearing results of a group of CPTs. """ def __init__( self, pile_tip_level_nap: Sequence[float], R_s_k: Sequence[float], R_b_k: Sequence[float], R_c_k: Sequence[float], R_s_d: Sequence[float], R_b_d: Sequence[float], R_c_d: Sequence[float], F_nk_cal_mean: Sequence[float], F_nk_k: Sequence[float], F_nk_d: Sequence[float], R_c_d_net: Sequence[float], F_c_k: Sequence[float], F_c_k_tot: Sequence[float], s_b: Sequence[float], s_e: Sequence[float], s_e_mean: Sequence[float], R_b_mob_ratio: Sequence[float], R_s_mob_ratio: Sequence[float], k_v_b: Sequence[float], k_v_1: Sequence[float], R_c_min: Sequence[float], R_c_max: Sequence[float], R_c_mean: Sequence[float], R_c_std: Sequence[float], R_s_mean: Sequence[float], R_b_mean: Sequence[float], var_coef: Sequence[float], n_cpts: Sequence[int], use_group_average: Sequence[bool], xi_normative: Sequence[str], xi_value: Sequence[float], cpt_Rc_min: Sequence[str], cpt_Rc_max: Sequence[str], cpt_normative: Sequence[str], ): """ Parameters ---------- pile_tip_level_nap: The pile-tip level [m] w.r.t. NAP. R_s_k: The characteristic value of the shaft bearingcapacity [kN]. R_b_k: The characteristic value of the bottom bearingcapacity [kN]. R_c_k: The characteristic value of the total compressive bearingcapacity [kN]. R_s_d: The design value of the shaft bearingcapacity [kN]. R_b_d: The design value of the bottom bearingcapacity [kN]. R_c_d: The design value of the total bearingcapacity [kN]. F_nk_cal_mean: The mean value of the calculated single-CPT negative friction forces [kN]. F_nk_k: The charactertistic value of the negative friction force [kN]. F_nk_d: The design value of the negative friction force [kN]. R_c_d_net: The net design value of the total bearingcapacity [kN] (netto = excluding design negative friction force.). F_c_k: The characteristic value of the load on the pile head (e.g. building load) [kN] F_c_k_tot: The characteristic value of the total compressive pile load [kN] (building-load + neg. friction force). s_b: The settlement of the pile bottom [mm]. s_e: The elastic shortening of the pile [mm]. s_e_mean: The mean of single-CPT results for elastic shortening of the pile [mm]. R_b_mob_ratio: The mobilisation ratio of the bottom bearing capacity [-]. R_s_mob_ratio: The mobilisation ratio of the shaft bearing capacity [-]. k_v_b: The 1-dimensional stiffness modulus at pile bottom [kN/m]. k_v_1: The 1-dimensional stiffness modulus at pile head [kN/m]. R_c_min: The minimum of the single-CPT values for the calculated bearingcapacity [kN]. R_c_max: The maximum of the single-CPT values for the calculated bearingcapacity [kN]. R_c_mean: The mean of the single-CPT values for the calculated bearingcapacity [kN]. R_c_std: The standard-deviation of the single-CPT values for the calculated bearingcapacity [kN]. R_s_mean: The mean of the single-CPT values for the calculated shaft bearingcapacity [kN]. R_b_mean: The mean of the single-CPT values for the calculated bottom bearingcapacity [kN]. var_coef: The variation coefficient [%] of the calculated bearing capacities in the group. n_cpts: The number of CPTs [-] that have been taken into account to establish the Xi value. use_group_average: If true, the group average is used for the calculation of characteristic group results. If false, the values of the normative CPT are used. xi_normative: The normative Xi (either Xi_3 or Xi_4) xi_value: The Xi value [-] that was applied to calculate the characteristic value of the total bearing capacity. cpt_Rc_min: The CPT with the lowest value for R_c_cal. cpt_Rc_max: The CPT with the highest value for R_c_cal. cpt_normative: The normative CPT. Can be "group average" if that was found to be the normative scenario. """ self.pile_tip_level_nap = ( np.array(pile_tip_level_nap).astype(np.float64).round(decimals=2) ) """The pile-tip level [m] w.r.t. NAP.""" self.R_s_k = np.array(R_s_k).astype(np.float64) """The characteristic value of the shaft bearingcapacity [kN].""" self.R_b_k = np.array(R_b_k).astype(np.float64) """The characteristic value of the bottom bearingcapacity [kN].""" self.R_c_k = np.array(R_c_k).astype(np.float64) """The characteristic value of the total compressive bearingcapacity [kN].""" self.R_s_d = np.array(R_s_d).astype(np.float64) """The design value of the shaft bearingcapacity [kN].""" self.R_b_d = np.array(R_b_d).astype(np.float64) """The design value of the bottom bearingcapacity [kN].""" self.R_c_d = np.array(R_c_d).astype(np.float64) """The design value of the total bearingcapacity [kN].""" self.F_nk_cal_mean = np.array(F_nk_cal_mean).astype(np.float64) """The mean value of the calculated single-CPT negative friction forces [kN].""" self.F_nk_k = np.array(F_nk_k).astype(np.float64) """The charactertistic value of the negative friction force [kN].""" self.F_nk_d = np.array(F_nk_d).astype(np.float64) """The design value of the negative friction force [kN].""" self.R_c_d_net = np.array(R_c_d_net).astype(np.float64) """The net design value of the total bearingcapacity [kN] (netto = excluding design negative friction force.).""" self.F_c_k = np.array(F_c_k).astype(np.float64) """The characteristic value of the load on the pile head (e.g. building load) [kN]""" self.F_c_k_tot = np.array(F_c_k_tot).astype(np.float64) """The characteristic value of the total compressive pile load [kN] (building-load + neg. friction force).""" self.s_b = np.array(s_b).astype(np.float64) """The settlement of the pile bottom [mm].""" self.s_e = np.array(s_e).astype(np.float64) """The elastic shortening of the pile [mm].""" self.s_e_mean = np.array(s_e_mean).astype(np.float64) """The mean of single-CPT results for elastic shortening of the pile [mm].""" self.R_b_mob_ratio = np.array(R_b_mob_ratio).astype(np.float64) """The mobilisation ratio of the bottom bearing capacity [-].""" self.R_s_mob_ratio = np.array(R_s_mob_ratio).astype(np.float64) """The mobilisation ratio of the shaft bearing capacity [-].""" self.k_v_b = np.array(k_v_b).astype(np.float64) """The 1-dimensional stiffness modulus at pile bottom [kN/m].""" self.k_v_1 = np.array(k_v_1).astype(np.float64) """The 1-dimensional stiffness modulus at pile head [kN/m].""" self.R_c_min = np.array(R_c_min).astype(np.float64) """The minimum of the single-CPT values for the calculated bearingcapacity [kN].""" self.R_c_max = np.array(R_c_max).astype(np.float64) """The maximum of the single-CPT values for the calculated bearingcapacity [kN].""" self.R_c_mean = np.array(R_c_mean).astype(np.float64) """The mean of the single-CPT values for the calculated bearingcapacity [kN].""" self.R_c_std = np.array(R_c_std).astype(np.float64) """The standard-deviation of the single-CPT values for the calculated bearingcapacity [kN].""" self.R_s_mean = np.array(R_s_mean).astype(np.float64) """The mean of the single-CPT values for the calculated shaft bearingcapacity [kN].""" self.R_b_mean = np.array(R_b_mean).astype(np.float64) """The mean of the single-CPT values for the calculated bottom bearingcapacity [kN].""" self.var_coef = np.array(var_coef).astype(np.float64) """The variation coefficient [%] of the calculated bearing capacities in the group.""" self.n_cpts = np.array(n_cpts).astype(np.int32) """The number of CPTs [-] that have been taken into account to establish the Xi value.""" self.use_group_average = np.array(use_group_average).astype(np.bool_) """If true, the group average is used for the calculation of characteristic group results. If false, the values of the normative CPT are used.""" self.xi_normative = np.array(xi_normative).astype(np.str_) """The normative Xi (either Xi_3 or Xi_4)""" self.xi_value = np.array(xi_value).astype(np.float64) """The Xi value [-] that was applied to calculate the characteristic value of the total bearing capacity.""" self.cpt_Rc_min = np.array(cpt_Rc_min).astype(np.str_) """The CPT with the lowest value for R_c_cal.""" self.cpt_Rc_max = np.array(cpt_Rc_max).astype(np.str_) """The CPT with the highest value for R_c_cal.""" self.cpt_normative = np.array(cpt_normative).astype(np.str_) """The normative CPT. Can be "group average" if that was found to be the normative scenario.""" for value in self.__dict__.values(): if not len(value) == len(self.pile_tip_level_nap): raise ValueError( "Inputs for CPTGroupResults dataclass must have same length." )
[docs] @lru_cache def to_pandas(self) -> pd.DataFrame: """The pandas.DataFrame representation""" return pd.DataFrame(self.__dict__).dropna(axis=0, how="all")
[docs] def plot_bearing_capacities( self, axes: Axes | None = None, figsize: Tuple[int, int] = (8, 10), add_legend: bool = True, **kwargs: Any, ) -> Axes: """ Plots the design value of the bearing capacity Rcd, based on a group of CPTs. Parameters ---------- axes: Optional `Axes` object where the bearing capacity data can be plotted on. If not provided, a new `plt.Figure` will be activated and the `Axes` object will be created and returned. figsize: Size of the activate figure, as the `plt.figure()` argument. add_legend: Add a legend to the `Axes` object **kwargs: All additional keyword arguments are passed to the `pyplot.subplots()` call. Returns ------- axes: The `Axes` object where the bearing capacities were plotted on. """ # Create axes objects if not provided if axes is not None: if not isinstance(axes, Axes): raise TypeError( "'axes' argument to plot_bearing_capacities() must be a `pyplot.axes.Axes` object or None." ) else: kwargs_subplot = { "figsize": figsize, "tight_layout": True, } kwargs_subplot.update(kwargs) _, axes = plt.subplots( 1, 1, **kwargs_subplot, ) if not isinstance(axes, Axes): raise ValueError( "Could not create Axes objects. This is probably due to invalid matplotlib keyword arguments. " ) axes.plot( self.R_c_d, self.pile_tip_level_nap, "-", label=r"$R_{c;d}$", ) axes.plot( self.F_nk_d, self.pile_tip_level_nap, ":", label=r"$F_{s;d}$ ", ) axes.set_xlabel("[kN]") axes.set_ylabel("[m] w.r.t. NAP") # set grid axes.grid() # Add legend if add_legend: axes.legend( loc="upper left", bbox_to_anchor=(1, 1), ) return axes
[docs]class SingleCPTBearingResultsContainer: """A container that holds multiple SingleCPTBearingResults objects""" def __init__(self, cpt_results_dict: Dict[str, SingleCPTBearingResults]): """ Parameters ---------- cpt_results_dict: A dictionary that maps the cpt-names to SingleCPTBearingResults objects. """ self._cpt_results_dict = cpt_results_dict
[docs] @classmethod def from_api_response( cls, cpt_results_list: list, cpt_input: dict ) -> "SingleCPTBearingResultsContainer": """ Instantiates the SingleCPTBearingResultsContainer object from the "cpts" array, which is returned in the response of a "compression/multiple-cpts/results" endpoint call. """ return cls( cpt_results_dict={ cpt_results["test_id"]: SingleCPTBearingResults.from_api_response( cpt_results_dict=cpt_results, ref_height=cpt_input[cpt_results["test_id"]]["ref_height"], surface_level_ref=cpt_input[cpt_results["test_id"]][ "surface_level_nap" ], x=cpt_input[cpt_results["test_id"]].get("location", {}).get("x"), y=cpt_input[cpt_results["test_id"]].get("location", {}).get("y"), ) for cpt_results in cpt_results_list } )
def __getitem__(self, test_id: str) -> SingleCPTBearingResults: if not isinstance(test_id, str): raise TypeError(f"Expected a test-id as a string, but got: {type(test_id)}") return self.get_cpt_results(test_id) @property def cpt_results_dict(self) -> Dict[str, SingleCPTBearingResults]: """The dictionary that maps the cpt-names to SingleCPTBearingResults objects.""" return self._cpt_results_dict @property def test_ids(self) -> List[str]: """The test-ids of the CPTs.""" return list(self.cpt_results_dict.keys()) @property def results(self) -> List[SingleCPTBearingResults]: """The computed results, as a list of SingleCPTBearingResults objects.""" return list(self.cpt_results_dict.values())
[docs] def get_cpt_results(self, test_id: str) -> SingleCPTBearingResults: """ Returns the `SingleCPTBearingResults` object for the provided test_id. """ if test_id not in self.cpt_results_dict.keys(): raise ValueError( f"No Cpt-results were calculated for this test-id: {test_id}. " "Please check the spelling or run a new calculation for this CPT." ) return self.cpt_results_dict[test_id]
[docs] def get_results_per_cpt(self, column_name: str) -> pd.DataFrame: """ Returns a pandas dataframe with a single result-item, organized per CPT (test-id) and pile-tip-level-nap. Parameters ---------- column_name: The name of the result-item / column name of the single-cpt-results table. """ if column_name not in self.to_pandas().columns or column_name in [ "pile_tip_level_nap", "test_id", ]: raise ValueError("Invalid column_name provided.") results = pd.pivot_table( self.to_pandas(), values=column_name, index="pile_tip_level_nap", columns="test_id", dropna=False, ) return results.sort_values("pile_tip_level_nap", ascending=False)
[docs] @lru_cache def to_pandas(self) -> pd.DataFrame: """Returns a total overview of all single-cpt results in a pandas.DataFrame representation.""" df_list: List[pd.DataFrame] = [] for test_id in self.cpt_results_dict: df = self.cpt_results_dict[test_id].table.to_pandas() df = df.assign(test_id=test_id) df_list.append(df) cpt_results_df = pd.concat(df_list) cpt_results_df = cpt_results_df.assign( pile_tip_level_nap=cpt_results_df.pile_tip_level_nap.round(1) ) return cpt_results_df
[docs]class MultiCPTBearingResults: """ Object that contains the results of a PileCore multi-cpt calculation. *Not meant to be instantiated by the user.* """ def __init__( self, cpt_results: SingleCPTBearingResultsContainer, group_results_table: CPTGroupResultsTable, pile_properties: PileProperties, gamma_f_nk: float, gamma_r_b: float, gamma_r_s: float, soil_load: float, ) -> None: """ Parameters ---------- cpt_results: The container object with single-CPT results group_results_table: The table object with CPT-group-results pile_properties: The PileProperties object gamma_f_nk: Safety factor for design-values of the negative sleeve friction force. [-] gamma_r_b: Safety factor, used to obtain design-values of the pile-tip bearingcapacity. [-] gamma_r_s: Safety factor, used to obtain design-values of the sleeve bearingcapacity. [-] soil_load: (Fictive) load on soil used to calculate soil settlement [kPa]. This is required and used to determine settlement of pile w.r.t. soil. """ self._pp = pile_properties self._gamma_f_nk = gamma_f_nk self._gamma_r_b = gamma_r_b self._gamma_r_s = gamma_r_s self._soil_load = soil_load self._cpt_results = cpt_results self._group_results_table = group_results_table
[docs] @classmethod def from_api_response( cls, response_dict: dict, cpt_input: dict ) -> "MultiCPTBearingResults": """ Build the object from the response payload of the PileCore endpoint "/compression/multi-cpt/results". """ cpt_results_dict = SingleCPTBearingResultsContainer.from_api_response( cpt_results_list=response_dict["cpts"], cpt_input=cpt_input ) group_results = response_dict["group_results"] return cls( cpt_results=cpt_results_dict, pile_properties=PileProperties.from_api_response( response_dict["pile_properties"] ), group_results_table=CPTGroupResultsTable( pile_tip_level_nap=group_results["pile_tip_level_nap"], R_s_k=group_results["R_s_k"], R_b_k=group_results["R_b_k"], R_c_k=group_results["R_c_k"], R_s_d=group_results["R_s_d"] if "R_s_d" in group_results else np.full_like( group_results["pile_tip_level_nap"], fill_value=np.nan ), # For backwards compatibility with PileCore-API < 2.9.0 R_b_d=group_results["R_b_d"] if "R_b_d" in group_results else np.full_like( group_results["pile_tip_level_nap"], fill_value=np.nan ), # For backwards compatibility with PileCore-API < 2.9.0, R_c_d=group_results["R_c_d"], F_nk_cal_mean=group_results["F_nk_cal_mean"], F_nk_k=group_results["F_nk_k"], F_nk_d=group_results["F_nk_d"], R_c_d_net=group_results["R_c_d_net"], F_c_k=group_results["F_c_k"], F_c_k_tot=group_results["F_c_k_tot"], s_b=group_results["s_b"], s_e=group_results["s_e"], s_e_mean=group_results["s_e_mean"], R_b_mob_ratio=group_results["R_b_mob_ratio"], R_s_mob_ratio=group_results["R_s_mob_ratio"], k_v_b=group_results["k_v_b"], k_v_1=group_results["k_v_1"], R_c_min=group_results["R_c_min"], R_c_max=group_results["R_c_max"], R_c_mean=group_results["R_c_mean"], R_c_std=group_results["R_c_std"], R_s_mean=group_results["R_s_mean"], R_b_mean=group_results["R_b_mean"], var_coef=group_results["var_coef"], n_cpts=group_results["n_cpts"], use_group_average=group_results["use_group_average"], xi_normative=group_results["xi_normative"], xi_value=group_results["xi_value"], cpt_Rc_min=group_results["cpt_Rc_min"], cpt_Rc_max=group_results["cpt_Rc_max"], cpt_normative=group_results["cpt_normative"], ), gamma_f_nk=response_dict["calculation_params"]["gamma_f_nk"], gamma_r_b=response_dict["calculation_params"]["gamma_r_b"], gamma_r_s=response_dict["calculation_params"]["gamma_r_s"], soil_load=response_dict["calculation_params"]["soil_load"], )
@property def pile_properties(self) -> PileProperties: """ The PileProperties object. """ return self._pp @property def cpt_results(self) -> SingleCPTBearingResultsContainer: """The SingleCPTBearingResultsContainer object.""" return self._cpt_results @property def cpt_names(self) -> List[str]: """The test-ids of the CPTs.""" return self.cpt_results.test_ids @property def group_results_table(self) -> CPTGroupResultsTable: """The CPTGroupResultsTable dataclass, containing the group results.""" return self._group_results_table
[docs] def boxplot( self, attribute: str, axes: Axes | None = None, figsize: Tuple[float, float] = (6.0, 6.0), show_sqrt: bool = False, **kwargs: Any, ) -> Axes: """ Plot a box and whisker plot for a given attribute. .. code-block:: none MIN Q1 median Q3 MAX |-----:-----| |--------| : |--------| |-----:-----| Parameters ---------- attribute: result attribute to create boxplot. Please note that the attribute name must be present in the `CPTResultsTable` and `CPTGroupResultsTable` class. axes: Optional `Axes` object where the boxplot data can be plotted on. If not provided, a new `plt.Figure` will be activated and the `Axes` object will be created and returned. figsize: Size of the activate figure, as the `plt.figure()` argument. show_sqrt: Add sqrt(2) bandwidth to figure **kwargs: All additional keyword arguments are passed to the `pyplot.subplots()` call. Returns ------- axes: The `Axes` object where the settlement curves were plotted on """ # validate attribute if ( attribute not in self.cpt_results.results[0].table.__dict__.keys() or attribute not in self.group_results_table.__dict__.keys() ): raise ValueError( f""" {attribute} is not present in CPTResultsTable or CPTGroupResultsTable class. Please select on of the following attributes: { set(self.cpt_results.results[0].table.__dict__.keys()) & set(self.group_results_table.__dict__.keys()) } """ ) # Create axes objects if not provided if axes is not None: if not isinstance(axes, Axes): raise ValueError( "'axes' argument to boxplot() must be a `pyplot.axes.Axes` object or None." ) else: kwargs_subplot = { "figsize": figsize, "tight_layout": True, } kwargs_subplot.update(kwargs) _, axes = plt.subplots( 1, 1, **kwargs_subplot, ) if not isinstance(axes, Axes): raise ValueError( "Could not create Axes objects. This is probably due to invalid matplotlib keyword arguments. " ) # Collect data from single calculation data = np.array( [ item.table.__getattribute__(attribute) for item in self.cpt_results.results ] ) # Draw a box and whisker plot axes.boxplot( np.flip(data, axis=0), tick_labels=np.flip(self.group_results_table.pile_tip_level_nap), whis=(0, 100), autorange=True, vert=False, patch_artist=True, showmeans=True, zorder=0, ) # ad additional bandwidth of sqrt(2) of the mean value if show_sqrt: axes.scatter( np.flip(data.mean(axis=0)) * np.sqrt(2), np.flip( np.arange(len(self.group_results_table.pile_tip_level_nap)) + 1 ), marker="^", color="tab:purple", zorder=1, ) axes.scatter( np.flip(data.mean(axis=0)) / np.sqrt(2), np.flip( np.arange(len(self.group_results_table.pile_tip_level_nap)) + 1 ), marker="^", color="tab:purple", zorder=1, ) # Draw group result over single result axes.scatter( np.flip(self.group_results_table.__getattribute__(attribute)), np.flip(np.arange(len(self.group_results_table.pile_tip_level_nap)) + 1), marker="o", color="tab:red", zorder=1, ) # Draw group result over single result for i, x in enumerate(data.mean(axis=0)): axes.annotate(f"{x.round(2)}", xy=(x, i + 1)) # add legend to figure axes.legend( handles=[ Patch(color=clr, label=key) for (key, clr) in { "Single;min:max": "black", "Single;Q25:Q75": "tab:blue", "Single;Q50": "tab:orange", "Single;mean": "tab:green", "Single;mean;sqrt": "tab:purple", "Group;normative": "tab:red", }.items() ], loc="upper left", bbox_to_anchor=(1, 1), title=f"Bandwidth {attribute}", ) # set label axes.set_ylabel("Depth [m NAP]") axes.set_xlabel(f"{attribute}") return axes
[docs] def plot_load_settlement( self, pile_tip_level_nap: float, axes: Axes | None = None, figsize: Tuple[float, float] = (6.0, 6.0), sb_max: float = 25, **kwargs: Any, ) -> Axes: """ Plot load settlement curve. Parameters ---------- pile_tip_level_nap: Pile tip level in [m NAP] for which to calculate and plot the load-settlement behavior. axes: Optional `Axes` object where the load-settlement data can be plotted on. If not provided, a new `plt.Figure` will be activated and the `Axes` object will be created and returned. figsize: Size of the activate figure, as the `plt.figure()` argument. sb_max: Default: 25 Maximum value for settlement at pile-tip level. **kwargs: All additional keyword arguments are passed to the `pyplot.subplots()` call. Returns ------- axes: The `Axes` object where the settlement curves were plotted on """ # Validate required properties are present if self._pp.pile_type.settlement_curve is None: raise ValueError( "No settlement curve is defined for the pile-type. " "Please define a settlement curve in the pile-type properties." ) # Validate axes if (axes is not None) and (not isinstance(axes, Axes)): raise ValueError( "'axes' argument to plot_load_settlement() must be a `pyplot.axes.Axes` object or None." ) # Get ptl index idx = int( np.argmin( abs( np.array(self.group_results_table.pile_tip_level_nap) - pile_tip_level_nap ) ) ) if ( abs(self.group_results_table.pile_tip_level_nap[idx] - pile_tip_level_nap) > 0.01 ): raise UserError( """No results have been calculated for the requested pile-tip-level. Please include this level in the pile-tip range parameter of the calculation.""" ) return get_load_settlement_plot( settlement_curve=self._pp.pile_type.settlement_curve, d_eq=self._pp.geometry.equiv_diameter_pile_tip, s_b=self.group_results_table.s_b[idx], f_c_k=self.group_results_table.F_c_k[idx], f_nk_k=self.group_results_table.F_nk_k[idx], r_b_k=self.group_results_table.R_b_k[idx], r_s_k=self.group_results_table.R_s_k[idx], rs_mob_ratio=self.group_results_table.R_s_mob_ratio[idx], rb_mob_ratio=self.group_results_table.R_b_mob_ratio[idx], sb_max=sb_max, axes=axes, figsize=figsize, **kwargs, )