py-pilecore 0.4.2__py3-none-any.whl → 0.5.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,335 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, List, Tuple
4
+
5
+ import numpy as np
6
+ from matplotlib import pyplot as plt
7
+ from matplotlib.axes import Axes
8
+ from numpy.typing import NDArray
9
+
10
+ from pypilecore.common.piles.geometry.components import (
11
+ RectPileGeometryComponent,
12
+ RoundPileGeometryComponent,
13
+ )
14
+ from pypilecore.common.piles.geometry.materials import Color, PileMaterial
15
+
16
+
17
+ class PileGeometry:
18
+ """The PileGeometry class represents the geometry of a pile."""
19
+
20
+ def __init__(
21
+ self,
22
+ components: List[RoundPileGeometryComponent | RectPileGeometryComponent],
23
+ materials: List[PileMaterial] | None = None,
24
+ pile_tip_factor_s: float | None = None,
25
+ beta_p: float | None = None,
26
+ ):
27
+ """
28
+ Represents the geometry of a pile.
29
+
30
+ Parameters:
31
+ -----------
32
+ components : list
33
+ A list of pile geometry components.
34
+ materials : list, optional
35
+ A list of materials used in the pile geometry, by default None.
36
+ pile_tip_factor_s : float, optional
37
+ The pile tip factor S, by default None.
38
+ beta_p : float, optional
39
+ The beta_p value, by default None.
40
+ """
41
+ self._components = components
42
+ self._materials = materials
43
+ self._pile_tip_factor_s = pile_tip_factor_s
44
+ self._beta_p = beta_p
45
+
46
+ @classmethod
47
+ def from_api_response(cls, geometry: dict) -> PileGeometry:
48
+ """
49
+ Instantiates a PileGeometry from a geometry object in the API response payload.
50
+
51
+ Parameters:
52
+ -----------
53
+ geometry: dict
54
+ A dictionary that is retrieved from the API response payload at "/pile_properties/geometry".
55
+
56
+ Returns:
57
+ --------
58
+ PileGeometry
59
+ A pile geometry.
60
+ """
61
+ components: List[RoundPileGeometryComponent | RectPileGeometryComponent] = []
62
+ for component in geometry["components"]:
63
+ if component["outer_shape"] == "round":
64
+ components.append(
65
+ RoundPileGeometryComponent.from_api_response(component)
66
+ )
67
+ else:
68
+ components.append(
69
+ RectPileGeometryComponent.from_api_response(component)
70
+ )
71
+
72
+ materials = []
73
+ if "materials" in geometry:
74
+ for material in geometry["materials"]:
75
+ materials.append(PileMaterial.from_api_response(material))
76
+
77
+ return cls(
78
+ components=components,
79
+ materials=materials,
80
+ pile_tip_factor_s=geometry["properties"].get("pile_tip_factor_s"),
81
+ beta_p=geometry["properties"]["beta_p"],
82
+ )
83
+
84
+ @property
85
+ def components(
86
+ self,
87
+ ) -> List[RoundPileGeometryComponent | RectPileGeometryComponent]:
88
+ """The components of the pile geometry"""
89
+ return self._components
90
+
91
+ @property
92
+ def materials(self) -> List[PileMaterial]:
93
+ """The materials used in the pile geometry"""
94
+ return self._materials if self._materials is not None else []
95
+
96
+ @property
97
+ def materials_dict(self) -> Dict[str, PileMaterial]:
98
+ """The materials used in the pile geometry as a dictionary with the material name as key"""
99
+ return {material.name: material for material in self.materials}
100
+
101
+ @property
102
+ def pile_tip_factor_s(self) -> float | None:
103
+ """The pile tip factor S of the pile geometry"""
104
+ return self._pile_tip_factor_s
105
+
106
+ @property
107
+ def beta_p(self) -> float | None:
108
+ """The beta_p value of the pile geometry"""
109
+ return self._beta_p
110
+
111
+ @property
112
+ def equiv_diameter_pile_tip(self) -> float:
113
+ """The equivalent diameter of the pile at the pile tip."""
114
+ return self.components[-1].equiv_tip_diameter
115
+
116
+ @property
117
+ def circumference_pile_tip(self) -> float:
118
+ """The outer-circumference of the pile at the pile tip."""
119
+ return self.components[-1].circumference
120
+
121
+ @property
122
+ def area_pile_tip(self) -> float:
123
+ """The area of the pile at the pile tip."""
124
+ return self.components[-1].area_full
125
+
126
+ def serialize_payload(self) -> Dict[str, list | dict]:
127
+ """
128
+ Serialize the pile geometry to a dictionary payload for the API.
129
+
130
+ Returns:
131
+ A dictionary payload containing the components, materials (if set), pile tip factor S (if set), and beta_p (if set).
132
+ """
133
+ components = [component.serialize_payload() for component in self.components]
134
+ payload: Dict[str, Any] = {"components": components}
135
+
136
+ if self.materials is not None and len(self.materials) > 0:
137
+ materials = [material.serialize_payload() for material in self.materials]
138
+ payload["materials"] = materials
139
+
140
+ custom_geom_properties: Dict[str, float] = {}
141
+ if self.pile_tip_factor_s is not None:
142
+ custom_geom_properties["pile_tip_factor_s"] = self.pile_tip_factor_s
143
+
144
+ if self.beta_p is not None:
145
+ custom_geom_properties["beta_p"] = self.beta_p
146
+
147
+ if len(custom_geom_properties.keys()) > 0:
148
+ payload["custom_properties"] = custom_geom_properties
149
+
150
+ return payload
151
+
152
+ def get_circum_vs_depth(
153
+ self,
154
+ pile_tip_level_nap: float | int,
155
+ pile_head_level_nap: float | int,
156
+ depth_nap: NDArray[np.floating],
157
+ ) -> NDArray[np.floating]:
158
+ """
159
+ Returns pile circumferences at requested depths.
160
+
161
+ Parameters
162
+ ---------_
163
+ pile_tip_level_nap : float
164
+ Pile tip level in [m] w.r.t. NAP.
165
+ pile_head_level_nap : float
166
+ Pile head level in [m] w.r.t. NAP.
167
+ depth_nap : np.array
168
+ Array with depths in [m] w.r.t. NAP.
169
+
170
+ Returns
171
+ -------
172
+ np.array
173
+ Array with pile circumferences at the requested `depth_nap` levels.
174
+ """
175
+ circum_vs_depth = np.zeros_like(depth_nap)
176
+
177
+ # Use the maximum circumference of all components at each depth.
178
+ for component in self.components:
179
+ circum_vs_depth = np.maximum(
180
+ circum_vs_depth,
181
+ component.get_circum_vs_depth(
182
+ pile_tip_level_nap=pile_tip_level_nap,
183
+ pile_head_level_nap=pile_head_level_nap,
184
+ depth_nap=depth_nap,
185
+ ),
186
+ )
187
+
188
+ return circum_vs_depth
189
+
190
+ def get_area_vs_depth(
191
+ self,
192
+ pile_tip_level_nap: float | int,
193
+ pile_head_level_nap: float | int,
194
+ depth_nap: NDArray[np.floating],
195
+ ) -> NDArray[np.floating]:
196
+ """
197
+ Returns cross-sectional area of the pile at requested depths.
198
+
199
+ Parameters
200
+ ---------_
201
+ pile_tip_level_nap : float
202
+ Pile tip level in [m] w.r.t. NAP.
203
+ pile_head_level_nap : float
204
+ Pile head level in [m] w.r.t. NAP.
205
+ depth_nap : np.array
206
+ Array with depths in [m] w.r.t. NAP.
207
+
208
+ Returns
209
+ -------
210
+ np.array
211
+ Array with pile areas at the requested `depth_nap` levels.
212
+ """
213
+ area_vs_depth = np.zeros_like(depth_nap)
214
+
215
+ # Use the maximum area of all components at each depth.
216
+ for component in self.components:
217
+ area_vs_depth = np.maximum(
218
+ area_vs_depth,
219
+ component.get_area_vs_depth(
220
+ pile_tip_level_nap=pile_tip_level_nap,
221
+ pile_head_level_nap=pile_head_level_nap,
222
+ depth_nap=depth_nap,
223
+ ),
224
+ )
225
+
226
+ return area_vs_depth
227
+
228
+ def plot(
229
+ self,
230
+ pile_tip_level_nap: float | int = -10,
231
+ pile_head_level_nap: float | int = 0,
232
+ figsize: Tuple[float, float] | None = (3, 9),
233
+ show: bool = True,
234
+ **kwargs: Any,
235
+ ) -> List[Axes]:
236
+ """
237
+ Plot the top-view of the pile at a specified depth.
238
+
239
+ Parameters
240
+ ----------
241
+ pile_tip_level_nap : float, optional
242
+ The pile tip level in m w.r.t. NAP, by default -10.
243
+ pile_head_level_nap : float, optional
244
+ The pile head level in m w.r.t. NAP, by default 0.
245
+ figsize : tuple, optional
246
+ The figure size (width, height) in inches, by default (6.0, 6.0).
247
+ show : bool, optional
248
+ Whether to display the plot, by default True.
249
+ **kwargs
250
+ Additional keyword arguments to pass to the `plt
251
+ """
252
+ kwargs_subplot = {
253
+ "figsize": figsize,
254
+ "gridspec_kw": {
255
+ "hspace": 0.15,
256
+ "height_ratios": [1, 4],
257
+ },
258
+ }
259
+
260
+ kwargs_subplot.update(kwargs)
261
+
262
+ height_ratio = (
263
+ kwargs_subplot["gridspec_kw"]["height_ratios"][1] # type: ignore
264
+ / kwargs_subplot["gridspec_kw"]["height_ratios"][0] # type: ignore
265
+ )
266
+
267
+ _, axes = plt.subplots(
268
+ 2,
269
+ 1,
270
+ **kwargs_subplot,
271
+ )
272
+
273
+ x_ticks = set([0.0])
274
+ y_ticks = set([0.0])
275
+
276
+ for component in self.components[::-1]:
277
+ facecolor = "grey"
278
+ if component.material in self.materials_dict:
279
+ material_color = self.materials_dict[component.material].color
280
+ if isinstance(material_color, Color):
281
+ facecolor = material_color.hex
282
+ elif isinstance(material_color, Color):
283
+ facecolor = (
284
+ material_color.red,
285
+ material_color.green,
286
+ material_color.blue,
287
+ )
288
+
289
+ component.plot_cross_section_exterior(
290
+ axes=axes[0],
291
+ facecolor=facecolor,
292
+ edgecolor="black",
293
+ axis_arg=None,
294
+ show=False,
295
+ )
296
+
297
+ component.plot_side_view(
298
+ pile_tip_level_nap=pile_tip_level_nap,
299
+ pile_head_level_nap=pile_head_level_nap,
300
+ axes=axes[1],
301
+ facecolor=facecolor,
302
+ axis_arg=None,
303
+ show=False,
304
+ )
305
+
306
+ x_ticks.add(component.cross_section_bounds[0])
307
+ x_ticks.add(component.cross_section_bounds[1])
308
+ y_ticks.add(component.cross_section_bounds[2])
309
+ y_ticks.add(component.cross_section_bounds[3])
310
+ axes[0].axis("scaled")
311
+ axes[0].set(aspect=1)
312
+ axes[1].axis("auto")
313
+ ax1_aspect = (
314
+ abs(axes[0].axis()[2] - axes[0].axis()[3])
315
+ / abs(axes[1].axis()[2] - axes[1].axis()[3])
316
+ * height_ratio
317
+ )
318
+ axes[1].set(aspect=ax1_aspect)
319
+
320
+ axes[0].spines[:].set_visible(False)
321
+ axes[1].spines[:].set_visible(False)
322
+
323
+ axes[0].set_xticks(ticks=list(x_ticks))
324
+ axes[0].set_yticks(ticks=list(y_ticks))
325
+ axes[1].set_xticks(ticks=list(x_ticks))
326
+ axes[1].set_yticks(
327
+ ticks=[pile_head_level_nap, pile_tip_level_nap],
328
+ labels=["Pile Head", "Pile Tip"],
329
+ )
330
+ axes[0].tick_params(axis="x", labelrotation=45)
331
+ axes[1].tick_params(axis="x", labelrotation=45)
332
+
333
+ if show:
334
+ plt.show()
335
+ return axes
@@ -0,0 +1,173 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Dict
4
+
5
+ MATERIALS = {
6
+ "concrete": {"name": "concrete", "elastic_modulus": 20000, "color": "#525252"},
7
+ "steel": {"name": "steel", "elastic_modulus": 195000, "color": "#E2E2E2"},
8
+ "wood": {"name": "wood", "elastic_modulus": 3600, "color": "#BD7205"},
9
+ "grout": {
10
+ "name": "grout",
11
+ "elastic_modulus": 15000,
12
+ "yield_stress": 1.5,
13
+ "color": "#8A8A8A",
14
+ },
15
+ "grout_extorted": {
16
+ "name": "grout_extorted",
17
+ "elastic_modulus": 20000,
18
+ "yield_stress": 2,
19
+ "color": "#8A8A8A",
20
+ },
21
+ }
22
+
23
+
24
+ class Color:
25
+ """The Color class represents an RGB color."""
26
+
27
+ def __init__(self, red: int, green: int, blue: int):
28
+ """
29
+ Represents an RGB color.
30
+
31
+ Parameters:
32
+ -----------
33
+ red : int
34
+ The red component of the RGB color.
35
+ green : int
36
+ The green component of the RGB color.
37
+ blue : int
38
+ The blue component of the RGB color.
39
+ """
40
+ self.red = red
41
+ self.green = green
42
+ self.blue = blue
43
+
44
+ @classmethod
45
+ def from_hex(cls, hex: str) -> Color:
46
+ """
47
+ Instantiate a Color object from a hexadecimal color string.
48
+
49
+ Parameters:
50
+ -----------
51
+ hex : str
52
+ The hexadecimal representation of the RGB color.
53
+ example: "#ff0000" for red.
54
+
55
+ Returns:
56
+ --------
57
+ Color
58
+ A Color object.
59
+ """
60
+ hex = hex.lstrip("#")
61
+ return cls(
62
+ red=int(hex[0:2], 16),
63
+ green=int(hex[2:4], 16),
64
+ blue=int(hex[4:6], 16),
65
+ )
66
+
67
+ @property
68
+ def hex(self) -> str:
69
+ """The hexadecimal representation of the RGB color. e.g. "#ff0000" for red."""
70
+ return "#{:02x}{:02x}{:02x}".format(self.red, self.green, self.blue)
71
+
72
+ def serialize_payload(self) -> Dict[str, int]:
73
+ """Serialize the RGB color to a dictionary payload for the API."""
74
+ return {"r": self.red, "g": self.green, "b": self.blue}
75
+
76
+
77
+ class PileMaterial:
78
+ """The PileMaterial class represents a material that can be used in a pile geometry component."""
79
+
80
+ def __init__(
81
+ self,
82
+ name: str,
83
+ elastic_modulus: float,
84
+ yield_stress: float | None = None,
85
+ color: Color | str | Dict[str, int] | None = None,
86
+ ):
87
+ """
88
+ Represents a material that can be used in a pile geometry component.
89
+
90
+ Parameters:
91
+ -----------
92
+ name : str
93
+ The name of the material.
94
+ elastic_modulus : float
95
+ The elastic modulus [MPa] of the material.
96
+ yield_stress : float, optional
97
+ The yield stress [MPa] of the material, by default None.
98
+ color : Color or str or dict, optional
99
+ The color of the material, by default None.
100
+ """
101
+ self._name = name
102
+ self._elastic_modulus = elastic_modulus
103
+ self._yield_stress = yield_stress
104
+ if isinstance(color, str):
105
+ color = Color.from_hex(hex=color)
106
+ elif isinstance(color, dict):
107
+ color = Color(
108
+ red=color["r"],
109
+ green=color["g"],
110
+ blue=color["b"],
111
+ )
112
+ self._color = color
113
+
114
+ @classmethod
115
+ def from_api_response(cls, material: dict) -> PileMaterial:
116
+ """
117
+ Instantiates a PileMaterial from a material object in the API response payload.
118
+
119
+ Args:
120
+ material: A dictionary that is retrieved from the API response payload at "/pile_properties/geometry/materials/[i]".
121
+ """
122
+ if isinstance(material["color"], str):
123
+ color = Color.from_hex(hex=material["color"])
124
+ else:
125
+ color = Color(
126
+ red=material["color"]["r"],
127
+ green=material["color"]["g"],
128
+ blue=material["color"]["b"],
129
+ )
130
+
131
+ return cls(
132
+ name=material["name"],
133
+ elastic_modulus=material["elastic_modulus"],
134
+ yield_stress=material.get("yield_stress"),
135
+ color=color,
136
+ )
137
+
138
+ @property
139
+ def name(self) -> str:
140
+ """The name of the material"""
141
+ return self._name
142
+
143
+ @property
144
+ def elastic_modulus(self) -> float:
145
+ """The elastic modulus [MPa] of the material"""
146
+ return self._elastic_modulus
147
+
148
+ @property
149
+ def yield_stress(self) -> float | None:
150
+ """The yield stress [MPa] of the material"""
151
+ return self._yield_stress
152
+
153
+ @property
154
+ def color(self) -> Color | Color | None:
155
+ """The color of the material"""
156
+ return self._color
157
+
158
+ def serialize_payload(self) -> Dict[str, str | float | Dict[str, int]]:
159
+ """
160
+ Serialize the material to a dictionary payload for the API.
161
+
162
+ Returns:
163
+ A dictionary payload containing the material's name, elastic modulus, and color.
164
+ """
165
+ payload: Dict[str, str | float | Dict[str, int]] = {
166
+ "name": self.name,
167
+ "elastic_modulus": self.elastic_modulus,
168
+ }
169
+
170
+ if self.color is not None:
171
+ payload["color"] = self.color.serialize_payload()
172
+
173
+ return payload