py-pilecore 0.4.2__py3-none-any.whl → 0.5.0__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.

Potentially problematic release.


This version of py-pilecore might be problematic. Click here for more details.

@@ -0,0 +1,521 @@
1
+ from __future__ import annotations
2
+
3
+ import math
4
+ from typing import Any, Literal, Tuple
5
+
6
+ import matplotlib.patches as patches
7
+ import numpy as np
8
+ from matplotlib import pyplot as plt
9
+ from matplotlib.axes import Axes
10
+ from numpy.typing import NDArray
11
+
12
+ from pypilecore.common.piles.geometry.components.common import (
13
+ PrimaryPileComponentDimension,
14
+ _BasePileGeometryComponent,
15
+ get_area_vs_depth,
16
+ get_circum_vs_depth,
17
+ get_component_bounds_nap,
18
+ instantiate_axes,
19
+ )
20
+
21
+
22
+ def calculate_equiv_tip_diameter(dim1: float, dim2: float | None = None) -> float:
23
+ """
24
+ Calculate the equivalent tip diameter of a rectangular pile.
25
+
26
+ Parameters
27
+ ----------
28
+ dim1, dim2 : float
29
+ The dimension of the rectangular pile (in any unit).
30
+
31
+ Returns
32
+ -------
33
+ float
34
+ The equivalent tip diameter of the rectangular pile (in the same unit as `dim1` and `dim2`).
35
+
36
+ Notes
37
+ -----
38
+ The equivalent tip diameter is calculated based on the dimensions of a rectangular pile. If the maximum dimension
39
+ (`b_max`) is greater than 1.5 times the minimum dimension (`a_min`), the equivalent tip diameter is equal to the
40
+ minimum dimension (`a_min`). Otherwise, it is calculated using the formula: 1.13 * `a_min` * sqrt(`b_max` / `a_min`).
41
+
42
+ Examples
43
+ --------
44
+ >>> calculate_equiv_tip_diameter(2.0, 3.0)
45
+ 2.0
46
+
47
+ >>> calculate_equiv_tip_diameter(2.0, 4.0)
48
+ 2.26
49
+ """
50
+ if dim2 is None:
51
+ dim2 = dim1
52
+
53
+ b_max = max(dim1, dim2)
54
+ a_min = min(dim1, dim2)
55
+
56
+ if b_max > (1.5 * a_min):
57
+ return a_min
58
+ return float(1.13 * a_min * math.sqrt(b_max / (a_min + 1e-12)))
59
+
60
+
61
+ class RectPileGeometryComponent(_BasePileGeometryComponent):
62
+ """The RectPileGeometryComponent class represents a rectangular pile-geometry component."""
63
+
64
+ def __init__(
65
+ self,
66
+ secondary_dimension: float,
67
+ primary_dimension: PrimaryPileComponentDimension,
68
+ tertiary_dimension: float | None = None,
69
+ inner_component: _BasePileGeometryComponent | None = None,
70
+ material: str | None = None,
71
+ ):
72
+ """
73
+ Represents a rectangular pile-geometry component.
74
+
75
+ Parameters:
76
+ -----------
77
+ secondary_dimension : float
78
+ The secondary dimension [m] of the pile-geometry component, which is the largest cross-sectional dimension.
79
+ primary_dimension : PrimaryPileComponentDimension
80
+ The primary dimension [m] of the pile-geometry component, which is measured along the primary axis of the pile.
81
+ tertiary_dimension : float, optional
82
+ The tertiary dimension [m] of the pile-geometry component, which is the smallest cross-sectional dimension.
83
+ inner_component : RoundPileGeometryComponent | RectPileGeometryComponent | None, optional
84
+ The component on the inside of the pile-geometry component, by default None.
85
+ material : str, optional
86
+ The material name of the pile-geometry component, by default None.
87
+ """
88
+ self._secondary_dimension = secondary_dimension
89
+ self._tertiary_dimension = tertiary_dimension
90
+ self._primary_dimension = primary_dimension
91
+ self._inner_component = inner_component
92
+ self._material = material
93
+
94
+ @classmethod
95
+ def from_api_response(
96
+ cls,
97
+ component: dict,
98
+ inner_component: _BasePileGeometryComponent | None = None,
99
+ ) -> RectPileGeometryComponent:
100
+ """
101
+ Instantiates a RectPileGeometryComponent from a component object in the API
102
+ response payload.
103
+
104
+ Parameters:
105
+ -----------
106
+ component: dict
107
+ A dictionary that is retrieved from the API response payload at "/pile_properties/geometry/components/[i]".
108
+ The component dictionary should have the following schema:
109
+
110
+ {
111
+ "secondary_dimension": float,
112
+ "tertiary_dimension": float,
113
+ "primary_dimension": {
114
+ "length": float,
115
+ "width": float
116
+ },
117
+ "material": str
118
+ }
119
+
120
+ - secondary_dimension (float): The secondary dimension of the rectangular pile component.
121
+ This dimension represents the dimension perpendicular to the primary dimension.
122
+ - tertiary_dimension (float): The tertiary dimension of the rectangular pile component.
123
+ This dimension represents the dimension perpendicular to both the primary and secondary dimensions.
124
+ - primary_dimension (dict): The primary dimension of the rectangular pile component.
125
+ - length (float): The length of the rectangular pile component.
126
+ This dimension represents the length of the pile along its primary axis.
127
+ - width (float): The width of the rectangular pile component.
128
+ This dimension represents the width of the pile along its secondary axis.
129
+ - material (str): The material of the rectangular pile component.
130
+
131
+ inner_component: RectPileGeometryComponent | RoundPileGeometryComponent | None, optional
132
+ The component on the inside of the pile-geometry component, by default None.
133
+
134
+ Returns:
135
+ --------
136
+ RectPileGeometryComponent
137
+ A rectangular pile-geometry component.
138
+
139
+ Example:
140
+ --------
141
+ >>> component = {
142
+ ... "secondary_dimension": 10,
143
+ ... "tertiary_dimension": 5,
144
+ ... "primary_dimension": {
145
+ ... "length": 20,
146
+ ... "width": 15
147
+ ... },
148
+ ... "material": "concrete"
149
+ ... }
150
+ >>> inner_component = None
151
+ >>> result = RectPileGeometryComponent.from_api_response(component, inner_component)
152
+ """
153
+ return cls(
154
+ secondary_dimension=component["secondary_dimension"],
155
+ tertiary_dimension=component["tertiary_dimension"],
156
+ primary_dimension=PrimaryPileComponentDimension.from_api_response(
157
+ component["primary_dimension"]
158
+ ),
159
+ inner_component=inner_component,
160
+ material=component["material"],
161
+ )
162
+
163
+ @property
164
+ def inner_component(
165
+ self,
166
+ ) -> _BasePileGeometryComponent | None:
167
+ """The component on the inside of the pile-geometry component"""
168
+ return self._inner_component
169
+
170
+ @property
171
+ def outer_shape(self) -> Literal["rectangle"]:
172
+ """The outer shape of the pile-geometry component"""
173
+ return "rectangle"
174
+
175
+ @property
176
+ def material(self) -> str | None:
177
+ """The material name of the pile-geometry component"""
178
+ return self._material
179
+
180
+ @property
181
+ def primary_dimension(self) -> PrimaryPileComponentDimension:
182
+ """
183
+ The primary dimension [m] of the pile-geometry component, which is measured along the primary axis of the pile.
184
+ """
185
+ return self._primary_dimension
186
+
187
+ @property
188
+ def secondary_dimension(self) -> float:
189
+ """
190
+ The secondary dimension [m] of the pile-geometry component, which is the largest cross-sectional dimension.
191
+ """
192
+ return self._secondary_dimension
193
+
194
+ @property
195
+ def tertiary_dimension(self) -> float:
196
+ """
197
+ The tertiary dimension [m] of the pile-geometry component, which is the smallest cross-sectional dimension.
198
+ """
199
+ if self._tertiary_dimension is not None:
200
+ return self._tertiary_dimension
201
+ return self.secondary_dimension
202
+
203
+ @property
204
+ def cross_section_bounds(self) -> Tuple[float, float, float, float]:
205
+ """Alias of the diameter [m] of the pile-geometry component"""
206
+ return (
207
+ -self.secondary_dimension / 2,
208
+ self.secondary_dimension / 2,
209
+ -self.tertiary_dimension / 2,
210
+ self.tertiary_dimension / 2,
211
+ )
212
+
213
+ @property
214
+ def circumference(self) -> float:
215
+ """The outer-circumference [m] of the pile-geometry component"""
216
+ return 2 * (self.secondary_dimension + self.tertiary_dimension)
217
+
218
+ @property
219
+ def equiv_tip_diameter(self) -> float:
220
+ """
221
+ Equivalent outer-diameter [m] of the component at the tip-level.
222
+ According to NEN-9997-1+C2_2017 paragraphs 1.5.2.106a and 7.6.2.3.(10)(e).
223
+
224
+ Specifically: returns self.tertiary_dimension
225
+ if self.primary_dimension > (1,5 * self.tertiary_dimension)
226
+ """
227
+ return calculate_equiv_tip_diameter(
228
+ self.tertiary_dimension, self.secondary_dimension
229
+ )
230
+
231
+ @property
232
+ def area_full(self) -> float:
233
+ """The full outer-area [m²] of the pile-geometry component, including any potential inner-components"""
234
+ return self.secondary_dimension * self.tertiary_dimension
235
+
236
+ def serialize_payload(self) -> dict:
237
+ """
238
+ Serialize the rectangular pile-geometry component to a dictionary payload for the API.
239
+
240
+ Returns:
241
+ A dictionary payload containing the outer shape, secondary dimension, tertiary dimension (if set), material, and primary dimension (if set).
242
+ """
243
+ payload = {
244
+ "outer_shape": self.outer_shape,
245
+ "primary_dimension": self.primary_dimension.serialize_payload(),
246
+ "secondary_dimension": self.secondary_dimension,
247
+ "material": self.material,
248
+ }
249
+ if self.tertiary_dimension is not None:
250
+ payload["tertiary_dimension"] = self.tertiary_dimension
251
+
252
+ return payload
253
+
254
+ def get_component_bounds_nap(
255
+ self,
256
+ pile_tip_level_nap: float | int,
257
+ pile_head_level_nap: float | int,
258
+ ) -> Tuple[float, float]:
259
+ """
260
+ Returns component head and tip level in NAP.
261
+
262
+ Parameters
263
+ ----------
264
+ pile_tip_level_nap : float
265
+ pile tip level in [m] w.r.t. NAP.
266
+ pile_head_level_nap : float
267
+ pile head level in [m] w.r.t. NAP.
268
+
269
+ Returns
270
+ -------
271
+ tuple
272
+ Tuple with component head and tip level in [m] w.r.t. NAP.
273
+ """
274
+ return get_component_bounds_nap(
275
+ pile_tip_level_nap=pile_tip_level_nap,
276
+ pile_head_level_nap=pile_head_level_nap,
277
+ component_primary_length=self.primary_dimension.length,
278
+ )
279
+
280
+ def get_circum_vs_depth(
281
+ self,
282
+ depth_nap: NDArray[np.floating],
283
+ pile_tip_level_nap: float | int,
284
+ pile_head_level_nap: float | int,
285
+ ) -> NDArray[np.floating]:
286
+ """
287
+ Returns component circumferences at requested depths.
288
+
289
+ Parameters
290
+ ----------
291
+ depth_nap : np.array
292
+ Array with depths in [m] w.r.t. NAP.
293
+ pile_tip_level_nap : float
294
+ pile tip level in [m] w.r.t. NAP.
295
+ pile_head_level_nap : float
296
+ pile head level in [m] w.r.t. NAP.
297
+
298
+ Returns
299
+ -------
300
+ np.array
301
+ Array with component circumferences at the depths in the depth parameter.
302
+ """
303
+ return get_circum_vs_depth(
304
+ depth_nap=depth_nap,
305
+ pile_tip_level_nap=pile_tip_level_nap,
306
+ pile_head_level_nap=pile_head_level_nap,
307
+ length=self.primary_dimension.length,
308
+ circumference=self.circumference,
309
+ )
310
+
311
+ def get_inner_area_vs_depth(
312
+ self,
313
+ depth_nap: NDArray[np.floating],
314
+ pile_tip_level_nap: float | int,
315
+ pile_head_level_nap: float | int,
316
+ ) -> NDArray[np.floating]:
317
+ """
318
+ Returns inner component areas at requested depths.
319
+
320
+ Parameters
321
+ ----------
322
+ depth_nap : np.array
323
+ Array with depths in [m] w.r.t. NAP.
324
+ pile_tip_level_nap : float
325
+ pile tip level in [m] w.r.t. NAP.
326
+ pile_head_level_nap : float
327
+ pile head level in [m] w.r.t. NAP.
328
+
329
+ Returns
330
+ -------
331
+ np.array
332
+ Array with inner component areas at the depths in the depth parameter.
333
+ """
334
+ if self.inner_component is None:
335
+ return np.zeros_like(depth_nap)
336
+
337
+ return self.inner_component.get_area_vs_depth(
338
+ depth_nap=depth_nap,
339
+ pile_tip_level_nap=pile_tip_level_nap,
340
+ pile_head_level_nap=pile_head_level_nap,
341
+ )
342
+
343
+ def get_area_vs_depth(
344
+ self,
345
+ depth_nap: NDArray[np.floating],
346
+ pile_tip_level_nap: float | int,
347
+ pile_head_level_nap: float | int,
348
+ ) -> NDArray[np.floating]:
349
+ """
350
+ Returns component areas at requested depths.
351
+
352
+ Parameters
353
+ ----------
354
+ depth_nap : np.array
355
+ Array with depths in [m] w.r.t. NAP.
356
+ pile_tip_level_nap : float
357
+ pile tip level in [m] w.r.t. NAP.
358
+ pile_head_level_nap : float
359
+ pile head level in [m] w.r.t. NAP.
360
+
361
+ Returns
362
+ -------
363
+ np.array
364
+ Array with component areas at the depths in the depth parameter.
365
+ """
366
+
367
+ (
368
+ component_head_level_nap,
369
+ component_tip_level_nap,
370
+ ) = self.get_component_bounds_nap(pile_tip_level_nap, pile_head_level_nap)
371
+
372
+ inner_area = self.get_inner_area_vs_depth(
373
+ depth_nap=depth_nap,
374
+ pile_tip_level_nap=pile_tip_level_nap,
375
+ pile_head_level_nap=pile_head_level_nap,
376
+ )
377
+
378
+ return get_area_vs_depth(
379
+ depth_nap=depth_nap,
380
+ area_full=self.area_full,
381
+ component_head_level_nap=component_head_level_nap,
382
+ component_tip_level_nap=component_tip_level_nap,
383
+ inner_area=inner_area,
384
+ )
385
+
386
+ def plot_cross_section_exterior(
387
+ self,
388
+ figsize: Tuple[float, float] = (6.0, 6.0),
389
+ facecolor: Tuple[float, float, float] | str | None = None,
390
+ axes: Axes | None = None,
391
+ axis_arg: bool | str | Tuple[float, float, float, float] | None = "auto",
392
+ show: bool = True,
393
+ **kwargs: Any,
394
+ ) -> Axes:
395
+ """
396
+ Plot the cross-section of the pile at a specified depth.
397
+
398
+ Parameters
399
+ ----------
400
+ figsize : tuple, optional
401
+ The figure size (width, height) in inches, by default (6.0, 6.0).
402
+ facecolor : tuple or str, optional
403
+ The face color of the pile cross-section, by default None.
404
+ axes : Axes
405
+ The axes object to plot the cross-section on.
406
+ axis_arg : bool or str or tuple, optional
407
+ The axis argument to pass to the `axes.axis()` function, by default "auto".
408
+ show : bool, optional
409
+ Whether to display the plot, by default True.
410
+ **kwargs
411
+ Additional keyword arguments to pass to the `plt.subplots()` function.
412
+ """
413
+ axes = instantiate_axes(
414
+ figsize=figsize,
415
+ axes=axes,
416
+ **kwargs,
417
+ )
418
+
419
+ x_offset = -self.secondary_dimension / 2
420
+ y_offset = -self.tertiary_dimension / 2
421
+
422
+ axes.add_patch(
423
+ patches.Rectangle(
424
+ (x_offset, y_offset),
425
+ self.secondary_dimension,
426
+ self.tertiary_dimension,
427
+ facecolor=facecolor,
428
+ edgecolor="black",
429
+ )
430
+ )
431
+ if axis_arg:
432
+ axes.axis(axis_arg)
433
+ if show:
434
+ plt.show()
435
+ return axes
436
+
437
+ def plot_side_view(
438
+ self,
439
+ bottom_boundary_nap: float | Literal["pile_tip"] = "pile_tip",
440
+ top_boundary_nap: float | Literal["pile_head"] = "pile_head",
441
+ pile_tip_level_nap: float | int = -10,
442
+ pile_head_level_nap: float | int = 0,
443
+ figsize: Tuple[float, float] = (6.0, 6.0),
444
+ facecolor: Tuple[float, float, float] | str | None = None,
445
+ axes: Axes | None = None,
446
+ axis_arg: bool | str | Tuple[float, float, float, float] | None = "scaled",
447
+ show: bool = True,
448
+ **kwargs: Any,
449
+ ) -> Axes:
450
+ """
451
+ Plot the side view of the component at a specified depth.
452
+
453
+ Parameters
454
+ ----------
455
+ bottom_boundary_nap : float or str, optional
456
+ The bottom boundary level of the plot, in m w.r.t. NAP. Default = "pile_tip".
457
+ top_boundary_nap : float or str, optional
458
+ The top boundary level of the plot, in m w.r.t. NAP. Default = "pile_head".
459
+ pile_tip_level_nap : float, optional
460
+ The pile tip level in m w.r.t. NAP. Default = -10.
461
+ pile_head_level_nap : float, optional
462
+ The pile head level in m w.r.t. NAP. Default = 0.
463
+ figsize : tuple, optional
464
+ The figure size (width, height) in inches, by default (6.0, 6.0).
465
+ facecolor : tuple or str, optional
466
+ The face color of the pile cross-section, by default None.
467
+ axes : Axes
468
+ The axes object to plot the cross-section on.
469
+ axis_arg : bool or str or tuple, optional
470
+ The axis argument to pass to the `axes.axis()` function, by default "auto".
471
+ show : bool, optional
472
+ Whether to display the plot, by default True.
473
+ **kwargs
474
+ Additional keyword arguments to pass to the `plt
475
+
476
+ Returns
477
+ -------
478
+ Axes
479
+ The axes object to plot the cross-section on.
480
+ """
481
+ axes = instantiate_axes(
482
+ figsize=figsize,
483
+ axes=axes,
484
+ **kwargs,
485
+ )
486
+
487
+ if top_boundary_nap == "pile_head":
488
+ top_boundary_nap = pile_head_level_nap
489
+
490
+ if bottom_boundary_nap == "pile_tip":
491
+ bottom_boundary_nap = pile_tip_level_nap
492
+
493
+ (
494
+ component_head_level_nap,
495
+ component_tip_level_nap,
496
+ ) = self.get_component_bounds_nap(pile_tip_level_nap, pile_head_level_nap)
497
+
498
+ if (
499
+ top_boundary_nap > component_tip_level_nap
500
+ and bottom_boundary_nap < component_head_level_nap
501
+ ):
502
+ z_offset = component_head_level_nap
503
+ height = (
504
+ max(component_tip_level_nap, bottom_boundary_nap)
505
+ - component_head_level_nap
506
+ )
507
+
508
+ axes.add_patch(
509
+ patches.Rectangle(
510
+ (self.cross_section_bounds[0], z_offset),
511
+ self.secondary_dimension,
512
+ height,
513
+ facecolor=facecolor,
514
+ )
515
+ )
516
+
517
+ if axis_arg:
518
+ axes.axis(axis_arg)
519
+ if show:
520
+ plt.show()
521
+ return axes