Source code for pycptcore.main

from __future__ import annotations

import json
from dataclasses import dataclass
from typing import Any, Literal, Sequence, Tuple, Union

import matplotlib.ticker as plticker
import pandas as pd
import requests
from matplotlib import pyplot as plt
from matplotlib.axes import Axes
from matplotlib.figure import Figure

Number = Union[float, int]
TickLoc = plticker.MultipleLocator(base=0.5)


@dataclass(frozen=True)
class Location:
    """DataClass that holds the standardized location information"""

    srs_name: str
    long: float
    lat: float

    @property
    def transform(self) -> dict:
        """transform EPSG 4326 to EPSG 28992 with epsg.io"""
        response = requests.get(
            f"https://epsg.io/trans?s_srs=4326&t_srs=28992&x={self.long}&y={self.lat}&format=json"
        )
        if response.ok:
            return json.loads(response.text)
        raise RuntimeError(response.content)


[docs]@dataclass(frozen=True) class LayerTable: """ Object that contains the Soil-layer data-traces. Attributes: ------------ geotechnicalSoilName: Sequence[str] geotechnical Soil Name related to the ISO lowerBoundary: Sequence[float] lower boundary of the layer [m] upperBoundary: Sequence[float] upper boundary of the layer [m] color: Sequence[str] hex color code mainComponent: Sequence[Literal["rocks", "gravel", "sand", "silt", "clay", "peat"]] main soil component cohesion: Sequence[float] cohesion of the layer [kPa] gamma_sat: Sequence[float] Saturated unit weight [kN/m^3] gamma_unsat: Sequence[float] unsaturated unit weight [kN/m^3] phi: Sequence[float] phi [degrees] undrainedShearStrength: Sequence[float] undrained shear strength [kPa] """ geotechnicalSoilName: Sequence[str] lowerBoundary: Sequence[float] upperBoundary: Sequence[float] color: Sequence[str] mainComponent: Sequence[Literal["rocks", "gravel", "sand", "silt", "clay", "peat"]] cohesion: Sequence[float] gamma_sat: Sequence[float] gamma_unsat: Sequence[float] phi: Sequence[float] undrainedShearStrength: Sequence[float] def __post_init__(self) -> None: raw_lengths = [len(values) for values in self.__dict__.values()] if len(list(set(raw_lengths))) > 1: raise ValueError("All values in this dataclass must have the same length.")
[docs] @classmethod def from_api_response(cls, response_dict: dict) -> "LayerTable": """ Stores the response of the CPTCore endpoint Parameters ---------- response_dict: The resulting response of a call to `classify/*` """ return cls( geotechnicalSoilName=response_dict.get("geotechnicalSoilName"), # type: ignore lowerBoundary=response_dict.get("lowerBoundary"), # type: ignore upperBoundary=response_dict.get("upperBoundary"), # type: ignore color=response_dict.get("color"), # type: ignore mainComponent=response_dict.get("mainComponent"), # type: ignore cohesion=response_dict.get("cohesion"), # type: ignore gamma_sat=response_dict.get("gamma_sat"), # type: ignore gamma_unsat=response_dict.get("gamma_unsat"), # type: ignore phi=response_dict.get("phi"), # type: ignore undrainedShearStrength=response_dict.get("undrainedShearStrength"), # type: ignore )
@property def dataframe(self) -> pd.DataFrame: """The pandas.DataFrame representation""" return pd.DataFrame(self.__dict__).dropna(axis="rows", how="any") # type: ignore
[docs] def plot( self, axes: plt.Axes | None = None, offset: float = 0, **kwargs: Any ) -> None: """ Plot the soil-layer in a subplot Parameters ---------- axes: `plt.Axes` object where the data can be plotted on. offset: offset sue to plot upper and lower boundary of the soil layer **kwargs: All additional keyword arguments are passed to the `pyplot.subplots()` call. """ if axes is None: _, axes = plt.subplots(**kwargs) for _, row in self.dataframe.iterrows(): lower_boundary = offset - row["lowerBoundary"] upper_boundary = offset - row["upperBoundary"] axes.fill_between( [0, 1], lower_boundary, upper_boundary, color=row["color"], ) # add annotate y = (lower_boundary - upper_boundary) / 2 + upper_boundary axes.annotate( text=row["geotechnicalSoilName"], xy=(0.25, y), fontsize=5, ) axes.invert_yaxis() axes.set_xticklabels([])
[docs]@dataclass(frozen=True) class CPTTable: """ Object that contains the CPT-related data-traces. Attributes: ------------ penetrationLength: Sequence[float] CPT penetrationLength [m] depthOffset: Sequence[float] CPT depth [m w.r.t. Reference] coneResistance: Sequence[float] cone resistance [Mpa] localFriction: Sequence[float] local friction [Mpa] frictionRatio: Sequence[float] friction ratio [-] """ penetrationLength: Sequence[float] depthOffset: Sequence[float] coneResistance: Sequence[float] localFriction: Sequence[float] | None frictionRatio: Sequence[float] | None def __post_init__(self) -> None: raw_lengths = [] for values in self.__dict__.values(): if values: raw_lengths.append(len(values)) if len(list(set(raw_lengths))) > 1: raise ValueError("All values in this dataclass must have the same length.")
[docs] @classmethod def from_api_response(cls, response_dict: dict) -> "CPTTable": """ Stores the response of the CPTCore endpoint Parameters ---------- response_dict: The resulting response of a call to `cpt/parse` """ return cls( penetrationLength=response_dict.get( "depth", response_dict.get("penetrationLength") ), depthOffset=response_dict.get("depthOffset"), # type: ignore coneResistance=response_dict.get("coneResistance"), # type: ignore localFriction=response_dict.get("localFriction"), frictionRatio=response_dict.get( "frictionRatio", response_dict.get("frictionRatioComputed") ), )
@property def dataframe(self) -> pd.DataFrame: """The pandas.DataFrame representation""" return pd.DataFrame(self.__dict__).dropna(axis="rows", how="any") # type: ignore
[docs] def plot_cone_resistance( self, axes: Axes | None = None, offset: float = 0, **kwargs: Any, ) -> None: """ Plots the cone resistance. Parameters ---------- axes: Optional Axes to plot on. offset: offset used to plot depth **kwargs: All additional keyword arguments are passed to the `pyplot.subplots()` call. """ if axes is None: _, axes = plt.subplots(**kwargs) axes.xaxis.set_ticks_position("top") axes.xaxis.set_label_position("top") axes.spines["top"].set_position(("axes", 1)) axes.set_xlim(0, 40) axes.set_xlabel("$q_c$ [MPa]") axes.xaxis.label.set_color("#2d2e87") axes.invert_yaxis() axes.plot( self.coneResistance, [offset - value for value in self.penetrationLength], color="#2d2e87", label="coneResistance", )
[docs] def plot_local_friction( self, axes: Axes | None = None, offset: float = 0, **kwargs: Any, ) -> None: """ Plots the local-friction data. Parameters ---------- axes: Optional Axes to plot on. offset: offset used to plot depth **kwargs: All additional keyword arguments are passed to the `pyplot.subplots()` call. """ if axes is None: _, axes = plt.subplots(**kwargs) axes.xaxis.set_ticks_position("top") axes.xaxis.set_label_position("top") axes.spines["top"].set_position(("axes", 1.05)) axes.set_xlabel("$f_s$ [MPa]") axes.set_xlim(0, 0.8) axes.xaxis.label.set_color("#e04913") axes.invert_yaxis() # add friction number subplot axes.plot( self.localFriction, [offset - value for value in self.penetrationLength], color="#e04913", label="localFriction", )
[docs] def plot_friction_ratio( self, axes: Axes | None = None, offset: float = 0, **kwargs: Any, ) -> None: """ Plots the friction-ratio data. Parameters ---------- axes: Optional Axes to plot on. offset: offset used to plot depth **kwargs: All additional keyword arguments are passed to the `pyplot.subplots()` call. """ if axes is None: _, axes = plt.subplots(**kwargs) axes.xaxis.set_ticks_position("top") axes.xaxis.set_label_position("top") axes.spines["top"].set_position(("axes", 1.1)) axes.set_xlabel("$R_f$ [%]") axes.set_xlim(0, 16) axes.invert_xaxis() axes.xaxis.label.set_color("tab:gray") axes.invert_yaxis() # add friction number subplot axes.plot( self.frictionRatio, [offset - value for value in self.penetrationLength], color="tab:gray", label="frictionRatio", )
[docs]@dataclass(frozen=True) class SoilProperties: """ A class for soil properties. Attributes: ------------ cpt_table: CPTTable CPT object layer_table: LayerTable layer table object location: Location spatial object verticalPositionReferencePoint: str vertical position reference point verticalPositionOffset: float vertical position offset [m w.r.t. reference] predrilledDepth: float predrilled depth [m] label: str CPT name groundwaterLevel: float groundwater level [m] """ cpt_table: CPTTable layer_table: LayerTable | None location: Location verticalPositionReferencePoint: str verticalPositionOffset: float predrilledDepth: float | None label: str groundwaterLevel: float | None
[docs] @classmethod def from_api_response( cls, response_parse: dict, response_classify: dict | None = None ) -> "SoilProperties": """ Stores the response of the CPTCore endpoint Parameters ---------- response_parse: The resulting response of a call to `cpt/parse` response_classify: The resulting response of a call to `classify/*` """ location = response_parse.get("location", {}) return cls( cpt_table=CPTTable.from_api_response(response_parse.get("data", {})), layer_table=LayerTable.from_api_response(response_classify) if response_classify else None, location=Location( lat=location.get("lat"), long=location.get("long"), srs_name=location.get("srs"), ), verticalPositionReferencePoint=response_parse.get( "verticalPositionReferencePoint", "Unknown" ), verticalPositionOffset=response_parse.get("verticalPositionOffset", 0), predrilledDepth=response_parse.get("predrilledDepth"), label=response_parse.get("label", "Unknown"), groundwaterLevel=response_parse.get("groundwaterLevel"), )
def set_layer_table_from_api_response(self, response_classify: dict) -> None: # bypass FrozenInstanceError object.__setattr__( self, "layer_table", LayerTable.from_api_response(response_classify) )
[docs] def plot( self, figsize: Tuple[float, float] = (10.0, 12.0), width_ratios: Tuple[float, float] = (1.0, 0.1), **kwargs: Any, ) -> Figure: """ Plots the CPT and soil table data. Parameters ---------- figsize: Size of the activate figure, as the `plt.figure()` argument. width_ratios: Tuple of width-ratios of the subplots, as the `plt.GridSpec` argument. **kwargs: All additional keyword arguments are passed to the `pyplot.subplots()` call. Returns ------- fig: The matplotlib Figure """ kwargs_subplot = { "gridspec_kw": {"width_ratios": width_ratios}, "sharey": "row", "figsize": figsize, "tight_layout": True, } kwargs_subplot.update(kwargs) fig, _ = plt.subplots( 1, 2, **kwargs_subplot, ) ax_qc, ax_layers = fig.axes ax_rf = ax_qc.twiny() ax_fs = ax_qc.twiny() # create grid major_ticks = range(0, 41, 5) minor_ticks = range(0, 41, 1) ax_qc.set_xticks(major_ticks) ax_qc.set_xticks(minor_ticks, minor=True) ax_qc.grid(which="both") ax_qc.grid(which="minor", alpha=0.2) ax_qc.grid(which="major", alpha=0.5) ax_qc.yaxis.set_major_locator(TickLoc) # Plot horizontal lines if self.groundwaterLevel: ax_qc.axhline( y=self.verticalPositionOffset - self.groundwaterLevel, color="tab:blue", linestyle="--", label="Groundwater level", ) if self.predrilledDepth: ax_qc.axhline( y=self.verticalPositionOffset - self.predrilledDepth, color="tab:brown", linestyle="--", label="Surface level", ) self.cpt_table.plot_cone_resistance(ax_qc, offset=self.verticalPositionOffset) self.cpt_table.plot_local_friction(ax_fs, offset=self.verticalPositionOffset) self.cpt_table.plot_friction_ratio(ax_rf, offset=self.verticalPositionOffset) if self.layer_table: self.layer_table.plot(axes=ax_layers, offset=self.verticalPositionOffset) return fig