xslope 0.1.2__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.
- xslope/__init__.py +1 -0
- xslope/_version.py +4 -0
- xslope/advanced.py +460 -0
- xslope/fem.py +2753 -0
- xslope/fileio.py +671 -0
- xslope/global_config.py +59 -0
- xslope/mesh.py +2719 -0
- xslope/plot.py +1484 -0
- xslope/plot_fem.py +1658 -0
- xslope/plot_seep.py +634 -0
- xslope/search.py +416 -0
- xslope/seep.py +2080 -0
- xslope/slice.py +1075 -0
- xslope/solve.py +1259 -0
- xslope-0.1.2.dist-info/LICENSE +196 -0
- xslope-0.1.2.dist-info/METADATA +56 -0
- xslope-0.1.2.dist-info/NOTICE +14 -0
- xslope-0.1.2.dist-info/RECORD +20 -0
- xslope-0.1.2.dist-info/WHEEL +5 -0
- xslope-0.1.2.dist-info/top_level.txt +1 -0
xslope/slice.py
ADDED
|
@@ -0,0 +1,1075 @@
|
|
|
1
|
+
# Copyright 2025 Norman L. Jones
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from math import sin, cos, tan, radians, atan, atan2, degrees, sqrt
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
import pandas as pd
|
|
19
|
+
from shapely.geometry import LineString, Point, MultiPoint, GeometryCollection
|
|
20
|
+
|
|
21
|
+
from .mesh import find_element_containing_point, interpolate_at_point
|
|
22
|
+
|
|
23
|
+
def get_circular_y_coordinates(x_coords, Xo, Yo, R):
|
|
24
|
+
"""
|
|
25
|
+
Calculate y-coordinates on a circular failure surface for given x-coordinates.
|
|
26
|
+
|
|
27
|
+
Parameters:
|
|
28
|
+
x_coords (array-like): X-coordinates to evaluate
|
|
29
|
+
Xo, Yo (float): Center coordinates of the circle
|
|
30
|
+
R (float): Radius of the circle
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
numpy.ndarray: Y-coordinates on the circle (bottom half)
|
|
34
|
+
"""
|
|
35
|
+
x_coords = np.asarray(x_coords)
|
|
36
|
+
# Calculate y-coordinates for the bottom half of the circle
|
|
37
|
+
# y = Yo - sqrt(R^2 - (x - Xo)^2)
|
|
38
|
+
dx_squared = (x_coords - Xo) ** 2
|
|
39
|
+
# Handle cases where x is outside the circle
|
|
40
|
+
valid_mask = dx_squared <= R ** 2
|
|
41
|
+
y_coords = np.full_like(x_coords, np.nan)
|
|
42
|
+
y_coords[valid_mask] = Yo - np.sqrt(R ** 2 - dx_squared[valid_mask])
|
|
43
|
+
return y_coords
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def get_circular_intersection_points(ground_surface, Xo, Yo, R, x_min, x_max):
|
|
47
|
+
"""
|
|
48
|
+
Find intersection points between a circular failure surface and ground surface.
|
|
49
|
+
|
|
50
|
+
Parameters:
|
|
51
|
+
ground_surface (LineString): Ground surface geometry
|
|
52
|
+
Xo, Yo, R (float): Circle parameters
|
|
53
|
+
x_min, x_max (float): X-range to search
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
tuple: (x_min, x_max, y_left, y_right, success)
|
|
57
|
+
"""
|
|
58
|
+
# Create a dense set of points on the circle for intersection testing
|
|
59
|
+
x_test = np.linspace(x_min, x_max, 1000)
|
|
60
|
+
y_circle = get_circular_y_coordinates(x_test, Xo, Yo, R)
|
|
61
|
+
|
|
62
|
+
# Create circle line for intersection
|
|
63
|
+
valid_mask = ~np.isnan(y_circle)
|
|
64
|
+
if not np.any(valid_mask):
|
|
65
|
+
return None, None, None, None, False
|
|
66
|
+
|
|
67
|
+
circle_coords = list(zip(x_test[valid_mask], y_circle[valid_mask]))
|
|
68
|
+
circle_line = LineString(circle_coords)
|
|
69
|
+
|
|
70
|
+
# Find intersections
|
|
71
|
+
intersections = circle_line.intersection(ground_surface)
|
|
72
|
+
|
|
73
|
+
if isinstance(intersections, Point):
|
|
74
|
+
points = [intersections]
|
|
75
|
+
elif isinstance(intersections, MultiPoint):
|
|
76
|
+
points = list(intersections.geoms)
|
|
77
|
+
elif isinstance(intersections, GeometryCollection):
|
|
78
|
+
points = [g for g in intersections.geoms if isinstance(g, Point)]
|
|
79
|
+
else:
|
|
80
|
+
points = []
|
|
81
|
+
|
|
82
|
+
if len(points) < 2:
|
|
83
|
+
return None, None, None, None, False
|
|
84
|
+
|
|
85
|
+
# Sort by x and take the two endpoints
|
|
86
|
+
points = sorted(points, key=lambda p: p.x)
|
|
87
|
+
x_min, x_max = points[0].x, points[-1].x
|
|
88
|
+
y_left, y_right = points[0].y, points[-1].y
|
|
89
|
+
|
|
90
|
+
return x_min, x_max, y_left, y_right, True
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def get_ground_surface_y_coordinates(x_coords, ground_surface):
|
|
94
|
+
"""
|
|
95
|
+
Get y-coordinates on the ground surface for given x-coordinates using interpolation.
|
|
96
|
+
|
|
97
|
+
Parameters:
|
|
98
|
+
x_coords (array-like): X-coordinates to evaluate
|
|
99
|
+
ground_surface (LineString): Ground surface geometry
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
numpy.ndarray: Y-coordinates on the ground surface
|
|
103
|
+
"""
|
|
104
|
+
x_coords = np.asarray(x_coords)
|
|
105
|
+
ground_coords = np.array(ground_surface.coords)
|
|
106
|
+
ground_x = ground_coords[:, 0]
|
|
107
|
+
ground_y = ground_coords[:, 1]
|
|
108
|
+
|
|
109
|
+
# Sort by x to ensure proper interpolation
|
|
110
|
+
sort_idx = np.argsort(ground_x)
|
|
111
|
+
ground_x = ground_x[sort_idx]
|
|
112
|
+
ground_y = ground_y[sort_idx]
|
|
113
|
+
|
|
114
|
+
# Interpolate y-coordinates
|
|
115
|
+
y_coords = np.interp(x_coords, ground_x, ground_y, left=np.nan, right=np.nan)
|
|
116
|
+
return y_coords
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def get_profile_layer_y_coordinates(x_coords, profile_lines):
|
|
120
|
+
"""
|
|
121
|
+
Get y-coordinates for each profile layer at given x-coordinates.
|
|
122
|
+
|
|
123
|
+
Parameters:
|
|
124
|
+
x_coords (array-like): X-coordinates to evaluate
|
|
125
|
+
profile_lines (list): List of profile layer lines
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
list: List of arrays, each containing y-coordinates for a profile layer
|
|
129
|
+
"""
|
|
130
|
+
x_coords = np.asarray(x_coords)
|
|
131
|
+
layer_y_coords = []
|
|
132
|
+
|
|
133
|
+
for line in profile_lines:
|
|
134
|
+
line_coords = np.array(line)
|
|
135
|
+
line_x = line_coords[:, 0]
|
|
136
|
+
line_y = line_coords[:, 1]
|
|
137
|
+
|
|
138
|
+
# Sort by x to ensure proper interpolation
|
|
139
|
+
sort_idx = np.argsort(line_x)
|
|
140
|
+
line_x = line_x[sort_idx]
|
|
141
|
+
line_y = line_y[sort_idx]
|
|
142
|
+
|
|
143
|
+
# Interpolate y-coordinates
|
|
144
|
+
y_coords = np.interp(x_coords, line_x, line_y, left=np.nan, right=np.nan)
|
|
145
|
+
layer_y_coords.append(y_coords)
|
|
146
|
+
|
|
147
|
+
return layer_y_coords
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def get_piezometric_y_coordinates(x_coords, piezo_line):
|
|
151
|
+
"""
|
|
152
|
+
Get y-coordinates on the piezometric surface for given x-coordinates.
|
|
153
|
+
|
|
154
|
+
Parameters:
|
|
155
|
+
x_coords (array-like): X-coordinates to evaluate
|
|
156
|
+
piezo_line (list): Piezometric line coordinates
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
numpy.ndarray: Y-coordinates on the piezometric surface
|
|
160
|
+
"""
|
|
161
|
+
if not piezo_line:
|
|
162
|
+
return np.full_like(x_coords, np.nan)
|
|
163
|
+
|
|
164
|
+
x_coords = np.asarray(x_coords)
|
|
165
|
+
piezo_coords = np.array(piezo_line)
|
|
166
|
+
piezo_x = piezo_coords[:, 0]
|
|
167
|
+
piezo_y = piezo_coords[:, 1]
|
|
168
|
+
|
|
169
|
+
# Sort by x to ensure proper interpolation
|
|
170
|
+
sort_idx = np.argsort(piezo_x)
|
|
171
|
+
piezo_x = piezo_x[sort_idx]
|
|
172
|
+
piezo_y = piezo_y[sort_idx]
|
|
173
|
+
|
|
174
|
+
# Interpolate y-coordinates
|
|
175
|
+
y_coords = np.interp(x_coords, piezo_x, piezo_y, left=np.nan, right=np.nan)
|
|
176
|
+
return y_coords
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def circle_polyline_intersections(Xo, Yo, R, polyline):
|
|
180
|
+
"""
|
|
181
|
+
Find intersection points between the bottom half of a circle and a polyline (LineString).
|
|
182
|
+
Returns a list of shapely Point objects.
|
|
183
|
+
"""
|
|
184
|
+
intersections = []
|
|
185
|
+
coords = list(polyline.coords)
|
|
186
|
+
for i in range(len(coords) - 1):
|
|
187
|
+
x1, y1 = coords[i]
|
|
188
|
+
x2, y2 = coords[i+1]
|
|
189
|
+
dx = x2 - x1
|
|
190
|
+
dy = y2 - y1
|
|
191
|
+
|
|
192
|
+
# Quadratic coefficients for t
|
|
193
|
+
a = dx**2 + dy**2
|
|
194
|
+
b = 2 * (dx * (x1 - Xo) + dy * (y1 - Yo))
|
|
195
|
+
c = (x1 - Xo)**2 + (y1 - Yo)**2 - R**2
|
|
196
|
+
|
|
197
|
+
discriminant = b**2 - 4*a*c
|
|
198
|
+
if discriminant < 0:
|
|
199
|
+
continue # No intersection
|
|
200
|
+
|
|
201
|
+
sqrt_disc = np.sqrt(discriminant)
|
|
202
|
+
for sign in [-1, 1]:
|
|
203
|
+
t = (-b + sign * sqrt_disc) / (2 * a)
|
|
204
|
+
if 0 <= t <= 1:
|
|
205
|
+
xi = x1 + t * dx
|
|
206
|
+
yi = y1 + t * dy
|
|
207
|
+
if yi < Yo: # Only keep points below the center (bottom half)
|
|
208
|
+
intersections.append(Point(xi, yi))
|
|
209
|
+
return intersections
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def get_sorted_intersections(failure_surface, ground_surface, circle_params=None):
|
|
213
|
+
"""
|
|
214
|
+
Find and sort the intersection points between the failure and ground surfaces,
|
|
215
|
+
pruning extras if the circle exits and re-enters the ground beyond the toe.
|
|
216
|
+
If circle_params is provided, use analytic intersection.
|
|
217
|
+
Returns:
|
|
218
|
+
success (bool), msg (str), points (list of shapely Point)
|
|
219
|
+
"""
|
|
220
|
+
if circle_params is not None:
|
|
221
|
+
Xo, Yo, R = circle_params['Xo'], circle_params['Yo'], circle_params['R']
|
|
222
|
+
points = circle_polyline_intersections(Xo, Yo, R, ground_surface)
|
|
223
|
+
else:
|
|
224
|
+
intersections = failure_surface.intersection(ground_surface)
|
|
225
|
+
if isinstance(intersections, MultiPoint):
|
|
226
|
+
points = list(intersections.geoms)
|
|
227
|
+
elif isinstance(intersections, Point):
|
|
228
|
+
points = [intersections]
|
|
229
|
+
elif isinstance(intersections, GeometryCollection):
|
|
230
|
+
points = [g for g in intersections.geoms if isinstance(g, Point)]
|
|
231
|
+
else:
|
|
232
|
+
points = []
|
|
233
|
+
|
|
234
|
+
# need at least two
|
|
235
|
+
if len(points) < 2:
|
|
236
|
+
return False, f"Expected at least 2 intersection points, but got {len(points)}.", None
|
|
237
|
+
|
|
238
|
+
# sort by x
|
|
239
|
+
points = sorted(points, key=lambda p: p.x)
|
|
240
|
+
|
|
241
|
+
# if exactly two, we're done
|
|
242
|
+
if len(points) == 2:
|
|
243
|
+
left, right = points[0], points[1]
|
|
244
|
+
tol = 1e-6
|
|
245
|
+
if abs(left.y - right.y) < tol:
|
|
246
|
+
return False, "Rejected: left-most and right-most intersection points have the same y-value (flat arc).", None
|
|
247
|
+
return True, "", points
|
|
248
|
+
|
|
249
|
+
# more than two: decide facing
|
|
250
|
+
y_first, y_last = points[0].y, points[-1].y
|
|
251
|
+
if y_first > y_last:
|
|
252
|
+
# right-facing: keep first two
|
|
253
|
+
pruned = points[:2]
|
|
254
|
+
else:
|
|
255
|
+
# left-facing: keep last two
|
|
256
|
+
pruned = points[-2:]
|
|
257
|
+
|
|
258
|
+
# sort those two again by x (just in case)
|
|
259
|
+
pruned = sorted(pruned, key=lambda p: p.x)
|
|
260
|
+
left, right = pruned[0], pruned[1]
|
|
261
|
+
tol = 1e-6
|
|
262
|
+
if abs(left.y - right.y) < tol:
|
|
263
|
+
return False, "Rejected: left-most and right-most intersection points have the same y-value (flat arc).", None
|
|
264
|
+
return True, "", pruned
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def adjust_ground_for_tcrack(ground_surface, x_center, tcrack_depth, right_facing):
|
|
268
|
+
# helper function to adjust the ground surface for tension crack
|
|
269
|
+
if tcrack_depth <= 0:
|
|
270
|
+
return ground_surface
|
|
271
|
+
|
|
272
|
+
new_coords = []
|
|
273
|
+
for x, y in ground_surface.coords:
|
|
274
|
+
if right_facing and x < x_center:
|
|
275
|
+
new_coords.append((x, y - tcrack_depth))
|
|
276
|
+
elif not right_facing and x > x_center:
|
|
277
|
+
new_coords.append((x, y - tcrack_depth))
|
|
278
|
+
else:
|
|
279
|
+
new_coords.append((x, y))
|
|
280
|
+
return LineString(new_coords)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def generate_failure_surface(ground_surface, circular, circle=None, non_circ=None, tcrack_depth=0):
|
|
284
|
+
"""
|
|
285
|
+
Generates a failure surface based on either a circular or non-circular definition.
|
|
286
|
+
|
|
287
|
+
Parameters:
|
|
288
|
+
ground_surface (LineString): The ground surface geometry.
|
|
289
|
+
circular (bool): Whether to use circular failure surface.
|
|
290
|
+
circle (dict, optional): Dictionary with keys 'Xo', 'Yo', 'Depth', and 'R'.
|
|
291
|
+
non_circ (list, optional): List of dicts with keys 'X', 'Y', and 'Movement'.
|
|
292
|
+
tcrack_depth (float, optional): Tension crack depth.
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
tuple: (success, result)
|
|
296
|
+
- If success is True:
|
|
297
|
+
result = (x_min, x_max, y_left, y_right, clipped_surface)
|
|
298
|
+
- If success is False:
|
|
299
|
+
result = error message string
|
|
300
|
+
"""
|
|
301
|
+
# --- Step 1: Build failure surface ---
|
|
302
|
+
if circular and circle:
|
|
303
|
+
Xo, Yo, depth, R = circle['Xo'], circle['Yo'], circle['Depth'], circle['R']
|
|
304
|
+
theta_range = np.linspace(np.pi, 2 * np.pi, 100)
|
|
305
|
+
arc = [(Xo + R * np.cos(t), Yo + R * np.sin(t)) for t in theta_range]
|
|
306
|
+
failure_coords = arc
|
|
307
|
+
failure_surface = LineString(arc)
|
|
308
|
+
elif non_circ:
|
|
309
|
+
failure_coords = [(pt['X'], pt['Y']) for pt in non_circ]
|
|
310
|
+
failure_surface = LineString(failure_coords)
|
|
311
|
+
else:
|
|
312
|
+
return False, "Either a circular or non-circular failure surface must be provided."
|
|
313
|
+
|
|
314
|
+
# --- Step 2: Intersect with original ground surface to determine slope facing ---
|
|
315
|
+
if circular and circle:
|
|
316
|
+
success, msg, points = get_sorted_intersections(failure_surface, ground_surface, circle_params=circle)
|
|
317
|
+
else:
|
|
318
|
+
success, msg, points = get_sorted_intersections(failure_surface, ground_surface)
|
|
319
|
+
if not success:
|
|
320
|
+
return False, msg
|
|
321
|
+
|
|
322
|
+
x_min, x_max = points[0].x, points[1].x
|
|
323
|
+
y_left, y_right = points[0].y, points[1].y
|
|
324
|
+
right_facing = y_left > y_right
|
|
325
|
+
x_center = 0.5 * (x_min + x_max)
|
|
326
|
+
|
|
327
|
+
# --- Step 3: If tension crack exists, adjust surface and re-intersect ---
|
|
328
|
+
if tcrack_depth > 0:
|
|
329
|
+
modified_surface = adjust_ground_for_tcrack(ground_surface, x_center, tcrack_depth, right_facing)
|
|
330
|
+
if circular and circle:
|
|
331
|
+
success, msg, points = get_sorted_intersections(failure_surface, modified_surface, circle_params=circle)
|
|
332
|
+
else:
|
|
333
|
+
success, msg, points = get_sorted_intersections(failure_surface, modified_surface)
|
|
334
|
+
if not success:
|
|
335
|
+
return False, msg
|
|
336
|
+
x_min, x_max = points[0].x, points[1].x
|
|
337
|
+
y_left, y_right = points[0].y, points[1].y
|
|
338
|
+
|
|
339
|
+
# --- Step 4: Clip the failure surface between intersection x-range ---
|
|
340
|
+
# Filter coordinates within the x-range
|
|
341
|
+
filtered_coords = [pt for pt in failure_coords if x_min <= pt[0] <= x_max]
|
|
342
|
+
|
|
343
|
+
# Add the exact intersection points if they're not already in the filtered list
|
|
344
|
+
left_intersection = (x_min, y_left)
|
|
345
|
+
right_intersection = (x_max, y_right)
|
|
346
|
+
|
|
347
|
+
# Check if intersection points are already in the filtered list (with tolerance)
|
|
348
|
+
tol = 1e-6
|
|
349
|
+
has_left = any(abs(pt[0] - x_min) < tol and abs(pt[1] - y_left) < tol for pt in filtered_coords)
|
|
350
|
+
has_right = any(abs(pt[0] - x_max) < tol and abs(pt[1] - y_right) < tol for pt in filtered_coords)
|
|
351
|
+
|
|
352
|
+
if not has_left:
|
|
353
|
+
filtered_coords.insert(0, left_intersection)
|
|
354
|
+
if not has_right:
|
|
355
|
+
filtered_coords.append(right_intersection)
|
|
356
|
+
|
|
357
|
+
# Sort by x-coordinate to ensure proper ordering
|
|
358
|
+
filtered_coords.sort(key=lambda pt: pt[0])
|
|
359
|
+
|
|
360
|
+
clipped_surface = LineString(filtered_coords)
|
|
361
|
+
|
|
362
|
+
return True, (x_min, x_max, y_left, y_right, clipped_surface)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def get_y_from_intersection(geom):
|
|
366
|
+
"""
|
|
367
|
+
Extracts the maximum Y-coordinate from a geometric intersection result.
|
|
368
|
+
|
|
369
|
+
This function handles different geometric types resulting from intersections,
|
|
370
|
+
including Point, MultiPoint, LineString, and GeometryCollection. If the input
|
|
371
|
+
geometry is not one of these or is empty, the function returns None.
|
|
372
|
+
|
|
373
|
+
Parameters:
|
|
374
|
+
geom (shapely.geometry.base.BaseGeometry): The geometry object from which
|
|
375
|
+
to extract the Y-coordinate(s).
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
float or None: The maximum Y-coordinate found in the geometry, or None if not found.
|
|
379
|
+
"""
|
|
380
|
+
if isinstance(geom, Point):
|
|
381
|
+
return geom.y
|
|
382
|
+
elif isinstance(geom, MultiPoint):
|
|
383
|
+
return max(pt.y for pt in geom.geoms)
|
|
384
|
+
elif isinstance(geom, LineString):
|
|
385
|
+
return max(y for _, y in geom.coords) if geom.coords else None
|
|
386
|
+
elif isinstance(geom, GeometryCollection):
|
|
387
|
+
pts = [g for g in geom.geoms if isinstance(g, Point)]
|
|
388
|
+
return max(pt.y for pt in pts) if pts else None
|
|
389
|
+
return None
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def calc_dload_resultant(x_l, y_lt, x_r, y_rt, qL, qR, dl):
|
|
393
|
+
"""
|
|
394
|
+
Compute:
|
|
395
|
+
- D : total resultant force from a trapezoidal load varying
|
|
396
|
+
linearly from intensity qL at (x_l,y_lt) to qR at (x_r,y_rt).
|
|
397
|
+
- d_x : x‐coordinate of the resultant's centroid on the top edge
|
|
398
|
+
- d_y : y‐coordinate of the resultant's centroid on the top edge
|
|
399
|
+
|
|
400
|
+
Parameters
|
|
401
|
+
----------
|
|
402
|
+
x_l, y_lt : float
|
|
403
|
+
Coordinates of the left‐end of the top edge.
|
|
404
|
+
x_r, y_rt : float
|
|
405
|
+
Coordinates of the right‐end of the top edge.
|
|
406
|
+
qL : float
|
|
407
|
+
Load intensity (force per unit length) at (x_l, y_lt).
|
|
408
|
+
qR : float
|
|
409
|
+
Load intensity (force per unit length) at (x_r, y_rt).
|
|
410
|
+
dl : float
|
|
411
|
+
Actual length along the inclined surface.
|
|
412
|
+
|
|
413
|
+
Returns
|
|
414
|
+
-------
|
|
415
|
+
D : float
|
|
416
|
+
Total resultant (area of trapezoid) = ½ (qL + qR) * dl
|
|
417
|
+
d_x : float
|
|
418
|
+
Global x‐coordinate of the centroid of that trapezoid
|
|
419
|
+
d_y : float
|
|
420
|
+
Global y‐coordinate of the centroid (lies on the line segment
|
|
421
|
+
between (x_l,y_lt) and (x_r,y_rt))
|
|
422
|
+
|
|
423
|
+
Notes
|
|
424
|
+
-----
|
|
425
|
+
1. If x_r == x_l (zero‐width slice), this will return D=0 and place
|
|
426
|
+
the "centroid" at (x_l, y_lt).
|
|
427
|
+
2. For a nonzero‐width trapezoid, the horizontal centroid‐offset from
|
|
428
|
+
x_l is:
|
|
429
|
+
x_offset = (x_r – x_l) * ( qL + 2 qR ) / [3 (qL + qR) ]
|
|
430
|
+
provided (qL + qR) ≠ 0. If qL + qR ≈ 0, it simply places the
|
|
431
|
+
centroid at the midpoint in x.
|
|
432
|
+
3. The vertical coordinate d_y is found by linear‐interpolation:
|
|
433
|
+
t = x_offset / (x_r – x_l)
|
|
434
|
+
d_y = y_lt + t ·(y_rt – y_lt)
|
|
435
|
+
|
|
436
|
+
"""
|
|
437
|
+
dx = x_r - x_l
|
|
438
|
+
|
|
439
|
+
# 1) Total resultant force (area under trapezoid) using actual length
|
|
440
|
+
D = 0.5 * (qL + qR) * dl
|
|
441
|
+
|
|
442
|
+
# 2) Horizontal centroid offset from left end
|
|
443
|
+
sum_q = qL + qR
|
|
444
|
+
if abs(sum_q) < 1e-12:
|
|
445
|
+
# nearly zero trapezoid => centroid at geometric midpoint
|
|
446
|
+
x_offset = dx * 0.5
|
|
447
|
+
else:
|
|
448
|
+
x_offset = dx * (qL + 2.0 * qR) / (3.0 * sum_q)
|
|
449
|
+
|
|
450
|
+
# 3) Global x‐coordinate of centroid
|
|
451
|
+
d_x = x_l + x_offset
|
|
452
|
+
|
|
453
|
+
# 4) Corresponding y‐coordinate by linear interpolation along top edge
|
|
454
|
+
t = x_offset / dx
|
|
455
|
+
d_y = y_lt + t * (y_rt - y_lt)
|
|
456
|
+
|
|
457
|
+
return D, d_x, d_y
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def generate_slices(slope_data, circle=None, non_circ=None, num_slices=40, debug=True):
|
|
461
|
+
|
|
462
|
+
"""
|
|
463
|
+
Generates vertical slices between the ground surface and a failure surface for slope stability analysis.
|
|
464
|
+
|
|
465
|
+
This function supports both circular and non-circular failure surfaces and computes
|
|
466
|
+
geometric and mechanical properties for each slice, including weight, base geometry,
|
|
467
|
+
water pressures, distributed loads, and reinforcement effects.
|
|
468
|
+
|
|
469
|
+
Parameters:
|
|
470
|
+
data (dict): Dictionary containing all input data
|
|
471
|
+
circle (dict, optional): Dictionary with keys 'Xo', 'Yo', 'Depth', and 'R' defining the circular failure surface.
|
|
472
|
+
non_circ (list, optional): List of dicts defining a non-circular failure surface with keys 'X', 'Y', and 'Movement'.
|
|
473
|
+
num_slices (int, optional): Desired number of slices to generate (default is 40).
|
|
474
|
+
debug (bool, optional): Whether to print debug information (default is True).
|
|
475
|
+
|
|
476
|
+
Returns:
|
|
477
|
+
tuple:
|
|
478
|
+
- pd.DataFrame: Slice table where each row includes geometry, strength, and external force values.
|
|
479
|
+
- shapely.geometry.LineString: The clipped failure surface between the ground surface intersections.
|
|
480
|
+
|
|
481
|
+
Notes:
|
|
482
|
+
- Supports Method A interpretation of reinforcement: T reduces driving forces.
|
|
483
|
+
- Handles pore pressure and distributed loads using linear interpolation at slice centers.
|
|
484
|
+
- Automatically includes all geometry breakpoints in slice generation.
|
|
485
|
+
- Must specify exactly one of 'circle' or 'non_circ'.
|
|
486
|
+
"""
|
|
487
|
+
|
|
488
|
+
# Unpack data
|
|
489
|
+
profile_lines = slope_data["profile_lines"]
|
|
490
|
+
ground_surface = slope_data["ground_surface"]
|
|
491
|
+
materials = slope_data["materials"]
|
|
492
|
+
piezo_line = slope_data["piezo_line"]
|
|
493
|
+
piezo_line2 = slope_data.get("piezo_line2", []) # Second piezometric line
|
|
494
|
+
gamma_w = slope_data["gamma_water"]
|
|
495
|
+
tcrack_depth = slope_data["tcrack_depth"]
|
|
496
|
+
tcrack_water = slope_data["tcrack_water"]
|
|
497
|
+
k_seismic = slope_data['k_seismic']
|
|
498
|
+
dloads = slope_data["dloads"]
|
|
499
|
+
dloads2 = slope_data.get("dloads2", [])
|
|
500
|
+
max_depth = slope_data["max_depth"]
|
|
501
|
+
|
|
502
|
+
# Determine failure surface type
|
|
503
|
+
if circle is not None:
|
|
504
|
+
circular = True
|
|
505
|
+
Xo, Yo, depth, R = circle['Xo'], circle['Yo'], circle['Depth'], circle['R']
|
|
506
|
+
else:
|
|
507
|
+
circular = False
|
|
508
|
+
|
|
509
|
+
# Prepare reinforcement lines data
|
|
510
|
+
reinf_lines_data = []
|
|
511
|
+
if slope_data.get("reinforce_lines"):
|
|
512
|
+
for line in slope_data["reinforce_lines"]:
|
|
513
|
+
xs = [pt["X"] for pt in line]
|
|
514
|
+
ts = [pt["T"] for pt in line]
|
|
515
|
+
geom = LineString([(pt["X"], pt["Y"]) for pt in line])
|
|
516
|
+
reinf_lines_data.append({"xs": xs, "ts": ts, "geom": geom})
|
|
517
|
+
|
|
518
|
+
ground_surface = LineString([(x, y) for x, y in ground_surface.coords])
|
|
519
|
+
|
|
520
|
+
# Generate failure surface
|
|
521
|
+
success, result = generate_failure_surface(ground_surface, circular, circle=circle, non_circ=non_circ, tcrack_depth=tcrack_depth)
|
|
522
|
+
if success:
|
|
523
|
+
x_min, x_max, y_left, y_right, clipped_surface = result
|
|
524
|
+
else:
|
|
525
|
+
return False, "Failed to generate surface:" + result
|
|
526
|
+
|
|
527
|
+
# Determine if the failure surface is right-facing
|
|
528
|
+
right_facing = y_left > y_right
|
|
529
|
+
|
|
530
|
+
# === BEGIN : Find set of points that should correspond to slice boundaries. ===
|
|
531
|
+
|
|
532
|
+
# Find set of points that are on the profile lines if the points are above the failure surface.
|
|
533
|
+
fixed_xs = set()
|
|
534
|
+
|
|
535
|
+
# Vectorized approach for profile line points
|
|
536
|
+
for line in profile_lines:
|
|
537
|
+
line_coords = np.array(line)
|
|
538
|
+
x_coords = line_coords[:, 0]
|
|
539
|
+
y_coords = line_coords[:, 1]
|
|
540
|
+
|
|
541
|
+
# Filter points within x-range
|
|
542
|
+
mask = (x_coords >= x_min) & (x_coords <= x_max)
|
|
543
|
+
x_filtered = x_coords[mask]
|
|
544
|
+
y_filtered = y_coords[mask]
|
|
545
|
+
|
|
546
|
+
if len(x_filtered) == 0:
|
|
547
|
+
continue
|
|
548
|
+
|
|
549
|
+
# Check if points are above the failure surface
|
|
550
|
+
if circular:
|
|
551
|
+
# Use parametric equation for circular failure surface
|
|
552
|
+
failure_y = get_circular_y_coordinates(x_filtered, Xo, Yo, R)
|
|
553
|
+
above_mask = y_filtered > failure_y
|
|
554
|
+
else:
|
|
555
|
+
# For non-circular, use geometric intersection (slower but necessary)
|
|
556
|
+
above_mask = np.zeros(len(x_filtered), dtype=bool)
|
|
557
|
+
for i, (x, y) in enumerate(zip(x_filtered, y_filtered)):
|
|
558
|
+
vertical_line = LineString([(x, -1e6), (x, 1e6)])
|
|
559
|
+
failure_y = get_y_from_intersection(clipped_surface.intersection(vertical_line))
|
|
560
|
+
if failure_y is not None and y > failure_y:
|
|
561
|
+
above_mask[i] = True
|
|
562
|
+
|
|
563
|
+
# Add points that are above the failure surface
|
|
564
|
+
fixed_xs.update(x_filtered[above_mask])
|
|
565
|
+
|
|
566
|
+
fixed_xs.update([x_min, x_max])
|
|
567
|
+
|
|
568
|
+
# Add transition points from dloads
|
|
569
|
+
if dloads:
|
|
570
|
+
fixed_xs.update(
|
|
571
|
+
pt['X'] for line in dloads for pt in line
|
|
572
|
+
if x_min <= pt['X'] <= x_max
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
# Add transition points from dloads2
|
|
576
|
+
if dloads2:
|
|
577
|
+
fixed_xs.update(
|
|
578
|
+
pt['X'] for line in dloads2 for pt in line
|
|
579
|
+
if x_min <= pt['X'] <= x_max
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
# Add transition points from non_circ
|
|
583
|
+
if non_circ:
|
|
584
|
+
fixed_xs.update(
|
|
585
|
+
pt['X'] for pt in non_circ
|
|
586
|
+
if x_min <= pt['X'] <= x_max
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
# Find intersections with profile lines and failure surface
|
|
590
|
+
if circular:
|
|
591
|
+
# For circular failure surfaces, we can use a more efficient approach
|
|
592
|
+
# by creating a dense circle representation and finding intersections
|
|
593
|
+
for line in profile_lines:
|
|
594
|
+
line_geom = LineString(line)
|
|
595
|
+
# Create a dense circle representation for intersection
|
|
596
|
+
theta_range = np.linspace(np.pi, 2 * np.pi, 200)
|
|
597
|
+
circle_coords = [(Xo + R * np.cos(t), Yo + R * np.sin(t)) for t in theta_range]
|
|
598
|
+
circle_line = LineString(circle_coords)
|
|
599
|
+
|
|
600
|
+
intersection = line_geom.intersection(circle_line)
|
|
601
|
+
if not intersection.is_empty:
|
|
602
|
+
if hasattr(intersection, 'x'):
|
|
603
|
+
fixed_xs.add(intersection.x)
|
|
604
|
+
elif hasattr(intersection, 'geoms'):
|
|
605
|
+
for geom in intersection.geoms:
|
|
606
|
+
if hasattr(geom, 'x'):
|
|
607
|
+
fixed_xs.add(geom.x)
|
|
608
|
+
else:
|
|
609
|
+
# For non-circular failure surfaces, use the original approach
|
|
610
|
+
for i in range(len(profile_lines)):
|
|
611
|
+
intersection = LineString(profile_lines[i]).intersection(clipped_surface)
|
|
612
|
+
if not intersection.is_empty:
|
|
613
|
+
if hasattr(intersection, 'x'):
|
|
614
|
+
fixed_xs.add(intersection.x)
|
|
615
|
+
elif hasattr(intersection, 'geoms'):
|
|
616
|
+
for geom in intersection.geoms:
|
|
617
|
+
if hasattr(geom, 'x'):
|
|
618
|
+
fixed_xs.add(geom.x)
|
|
619
|
+
|
|
620
|
+
# Find intersections with piezometric lines
|
|
621
|
+
if piezo_line:
|
|
622
|
+
piezo_geom1 = LineString(piezo_line)
|
|
623
|
+
if circular:
|
|
624
|
+
# Use dense circle representation for intersection
|
|
625
|
+
theta_range = np.linspace(np.pi, 2 * np.pi, 200)
|
|
626
|
+
circle_coords = [(Xo + R * np.cos(t), Yo + R * np.sin(t)) for t in theta_range]
|
|
627
|
+
circle_line = LineString(circle_coords)
|
|
628
|
+
intersection1 = piezo_geom1.intersection(circle_line)
|
|
629
|
+
else:
|
|
630
|
+
intersection1 = piezo_geom1.intersection(clipped_surface)
|
|
631
|
+
|
|
632
|
+
if not intersection1.is_empty:
|
|
633
|
+
if hasattr(intersection1, 'x'):
|
|
634
|
+
# Single point intersection
|
|
635
|
+
if x_min <= intersection1.x <= x_max:
|
|
636
|
+
fixed_xs.add(intersection1.x)
|
|
637
|
+
elif hasattr(intersection1, 'geoms'):
|
|
638
|
+
# Multiple points or line intersection
|
|
639
|
+
for geom in intersection1.geoms:
|
|
640
|
+
if hasattr(geom, 'x') and x_min <= geom.x <= x_max:
|
|
641
|
+
fixed_xs.add(geom.x)
|
|
642
|
+
|
|
643
|
+
if piezo_line2:
|
|
644
|
+
piezo_geom2 = LineString(piezo_line2)
|
|
645
|
+
if circular:
|
|
646
|
+
theta_range = np.linspace(np.pi, 2 * np.pi, 200)
|
|
647
|
+
circle_coords = [(Xo + R * np.cos(t), Yo + R * np.sin(t)) for t in theta_range]
|
|
648
|
+
circle_line = LineString(circle_coords)
|
|
649
|
+
intersection2 = piezo_geom2.intersection(circle_line)
|
|
650
|
+
else:
|
|
651
|
+
intersection2 = piezo_geom2.intersection(clipped_surface)
|
|
652
|
+
|
|
653
|
+
if not intersection2.is_empty:
|
|
654
|
+
if hasattr(intersection2, 'x'):
|
|
655
|
+
# Single point intersection
|
|
656
|
+
if x_min <= intersection2.x <= x_max:
|
|
657
|
+
fixed_xs.add(intersection2.x)
|
|
658
|
+
elif hasattr(intersection2, 'geoms'):
|
|
659
|
+
# Multiple points or line intersection
|
|
660
|
+
for geom in intersection2.geoms:
|
|
661
|
+
if hasattr(geom, 'x') and x_min <= geom.x <= x_max:
|
|
662
|
+
fixed_xs.add(geom.x)
|
|
663
|
+
|
|
664
|
+
# Remove duplicate points that are very close to each other
|
|
665
|
+
tolerance = 1e-6
|
|
666
|
+
cleaned_xs = []
|
|
667
|
+
for x in sorted(fixed_xs):
|
|
668
|
+
if not cleaned_xs or abs(x - cleaned_xs[-1]) > tolerance:
|
|
669
|
+
cleaned_xs.append(x)
|
|
670
|
+
|
|
671
|
+
fixed_xs = cleaned_xs
|
|
672
|
+
|
|
673
|
+
# Generate slice boundaries
|
|
674
|
+
segment_lengths = [fixed_xs[i + 1] - fixed_xs[i] for i in range(len(fixed_xs) - 1)]
|
|
675
|
+
total_length = sum(segment_lengths)
|
|
676
|
+
all_xs = [fixed_xs[0]]
|
|
677
|
+
for i in range(len(fixed_xs) - 1):
|
|
678
|
+
x_start = fixed_xs[i]
|
|
679
|
+
x_end = fixed_xs[i + 1]
|
|
680
|
+
segment_length = x_end - x_start
|
|
681
|
+
n_subdiv = max(1, int(round((segment_length / total_length) * num_slices)))
|
|
682
|
+
xs = np.linspace(x_start, x_end, n_subdiv + 1).tolist()
|
|
683
|
+
all_xs.extend(xs[1:])
|
|
684
|
+
|
|
685
|
+
# === END : Find set of points that should correspond to slice boundaries. ===
|
|
686
|
+
|
|
687
|
+
# Remove thin slices (width < 1e-2), including at the ends
|
|
688
|
+
min_width = 1e-2
|
|
689
|
+
cleaned_xs = [all_xs[0]]
|
|
690
|
+
for x in all_xs[1:]:
|
|
691
|
+
if abs(x - cleaned_xs[-1]) >= min_width:
|
|
692
|
+
cleaned_xs.append(x)
|
|
693
|
+
|
|
694
|
+
# If the last slice is thin, merge it with the previous one
|
|
695
|
+
if len(cleaned_xs) > 2 and abs(cleaned_xs[-1] - cleaned_xs[-2]) < min_width:
|
|
696
|
+
cleaned_xs.pop(-2)
|
|
697
|
+
|
|
698
|
+
all_xs = cleaned_xs
|
|
699
|
+
|
|
700
|
+
# Pre-compute all y-coordinates for efficiency
|
|
701
|
+
slice_x_coords = np.array(all_xs)
|
|
702
|
+
slice_centers = (slice_x_coords[:-1] + slice_x_coords[1:]) / 2
|
|
703
|
+
|
|
704
|
+
# Get failure surface y-coordinates
|
|
705
|
+
if circular:
|
|
706
|
+
# Use parametric equations for circular failure surface
|
|
707
|
+
y_lb_all = get_circular_y_coordinates(slice_x_coords[:-1], Xo, Yo, R)
|
|
708
|
+
y_rb_all = get_circular_y_coordinates(slice_x_coords[1:], Xo, Yo, R)
|
|
709
|
+
y_cb_all = get_circular_y_coordinates(slice_centers, Xo, Yo, R)
|
|
710
|
+
else:
|
|
711
|
+
# For non-circular, we need to use geometric intersections
|
|
712
|
+
y_lb_all = np.array([get_y_from_intersection(clipped_surface.intersection(LineString([(x, -1e6), (x, 1e6)]))) for x in slice_x_coords[:-1]])
|
|
713
|
+
y_rb_all = np.array([get_y_from_intersection(clipped_surface.intersection(LineString([(x, -1e6), (x, 1e6)]))) for x in slice_x_coords[1:]])
|
|
714
|
+
y_cb_all = np.array([get_y_from_intersection(clipped_surface.intersection(LineString([(x, -1e6), (x, 1e6)]))) for x in slice_centers])
|
|
715
|
+
|
|
716
|
+
# Get ground surface y-coordinates
|
|
717
|
+
y_lt_all = get_ground_surface_y_coordinates(slice_x_coords[:-1], ground_surface)
|
|
718
|
+
y_rt_all = get_ground_surface_y_coordinates(slice_x_coords[1:], ground_surface)
|
|
719
|
+
y_ct_all = get_ground_surface_y_coordinates(slice_centers, ground_surface)
|
|
720
|
+
|
|
721
|
+
# Get profile layer y-coordinates
|
|
722
|
+
profile_y_coords = get_profile_layer_y_coordinates(slice_centers, profile_lines)
|
|
723
|
+
|
|
724
|
+
# Get piezometric y-coordinates
|
|
725
|
+
piezo_y_all = get_piezometric_y_coordinates(slice_centers, piezo_line)
|
|
726
|
+
piezo_y2_all = get_piezometric_y_coordinates(slice_centers, piezo_line2)
|
|
727
|
+
|
|
728
|
+
# Interpolation functions for distributed loads
|
|
729
|
+
dload_interp_funcs = []
|
|
730
|
+
if dloads:
|
|
731
|
+
for line in dloads:
|
|
732
|
+
xs = [pt['X'] for pt in line]
|
|
733
|
+
normals = [pt['Normal'] for pt in line]
|
|
734
|
+
dload_interp_funcs.append(lambda x, xs=xs, normals=normals: np.interp(x, xs, normals, left=0, right=0))
|
|
735
|
+
|
|
736
|
+
# Interpolation functions for second set of distributed loads
|
|
737
|
+
dload2_interp_funcs = []
|
|
738
|
+
if dloads2:
|
|
739
|
+
for line in dloads2:
|
|
740
|
+
xs = [pt['X'] for pt in line]
|
|
741
|
+
normals = [pt['Normal'] for pt in line]
|
|
742
|
+
dload2_interp_funcs.append(lambda x, xs=xs, normals=normals: np.interp(x, xs, normals, left=0, right=0))
|
|
743
|
+
|
|
744
|
+
# Generate slices
|
|
745
|
+
slices = []
|
|
746
|
+
for i in range(len(all_xs) - 1):
|
|
747
|
+
x_l, x_r = slice_x_coords[i], slice_x_coords[i + 1]
|
|
748
|
+
x_c = slice_centers[i]
|
|
749
|
+
dx = x_r - x_l
|
|
750
|
+
|
|
751
|
+
# Get pre-computed y-coordinates
|
|
752
|
+
y_lb = y_lb_all[i]
|
|
753
|
+
y_rb = y_rb_all[i]
|
|
754
|
+
y_cb = y_cb_all[i]
|
|
755
|
+
y_lt = y_lt_all[i]
|
|
756
|
+
y_rt = y_rt_all[i]
|
|
757
|
+
y_ct = y_ct_all[i]
|
|
758
|
+
|
|
759
|
+
# Skip if any coordinates are invalid
|
|
760
|
+
if any(np.isnan([y_lb, y_rb, y_cb, y_lt, y_rt, y_ct])):
|
|
761
|
+
continue
|
|
762
|
+
|
|
763
|
+
# Calculate beta (slope angle of the top edge) in degrees
|
|
764
|
+
beta = degrees(atan2(y_rt - y_lt, x_r - x_l))
|
|
765
|
+
if right_facing:
|
|
766
|
+
beta = -beta
|
|
767
|
+
|
|
768
|
+
# Calculate dl for the top surface (for distributed loads)
|
|
769
|
+
dl_top = sqrt((x_r - x_l)**2 + (y_rt - y_lt)**2)
|
|
770
|
+
|
|
771
|
+
# Calculate layer heights using pre-computed profile coordinates
|
|
772
|
+
heights = []
|
|
773
|
+
soil_weight = 0
|
|
774
|
+
base_material_idx = None
|
|
775
|
+
sum_gam_h_y = 0 # for calculating center of gravity of slice
|
|
776
|
+
sum_gam_h = 0 # ditto
|
|
777
|
+
|
|
778
|
+
for mat_index, layer_y in enumerate(profile_y_coords):
|
|
779
|
+
layer_top_y = layer_y[i]
|
|
780
|
+
|
|
781
|
+
# Bottom: highest of all other profile lines at x, or failure surface
|
|
782
|
+
layer_bot_y = y_cb # Start with failure surface as default bottom
|
|
783
|
+
for j in range(mat_index + 1, len(profile_y_coords)):
|
|
784
|
+
next_y = profile_y_coords[j][i]
|
|
785
|
+
if not np.isnan(next_y) and next_y > layer_bot_y:
|
|
786
|
+
# Take the highest of the lower profile lines
|
|
787
|
+
layer_bot_y = next_y
|
|
788
|
+
|
|
789
|
+
if np.isnan(layer_top_y) or np.isnan(layer_bot_y):
|
|
790
|
+
h = 0
|
|
791
|
+
else:
|
|
792
|
+
overlap_top = min(y_ct, layer_top_y)
|
|
793
|
+
overlap_bot = max(y_cb, layer_bot_y)
|
|
794
|
+
h = max(0, overlap_top - overlap_bot)
|
|
795
|
+
sum_gam_h_y += h * materials[mat_index]['gamma'] * (overlap_top + overlap_bot) / 2
|
|
796
|
+
sum_gam_h += h * materials[mat_index]['gamma']
|
|
797
|
+
|
|
798
|
+
heights.append(h)
|
|
799
|
+
soil_weight += h * materials[mat_index]['gamma'] * dx
|
|
800
|
+
|
|
801
|
+
if h > 0:
|
|
802
|
+
base_material_idx = mat_index
|
|
803
|
+
|
|
804
|
+
# Center of gravity
|
|
805
|
+
y_cg = (sum_gam_h_y) / sum_gam_h if sum_gam_h > 0 else None
|
|
806
|
+
|
|
807
|
+
# Distributed load
|
|
808
|
+
qC = sum(func(x_c) for func in dload_interp_funcs) if dload_interp_funcs else 0 # intensity at center
|
|
809
|
+
if qC > 0: # We need to check qC to distinguish between a linear ramp up (down) and the case where the load starts or ends on one of the sides
|
|
810
|
+
qL = sum(func(x_l) for func in dload_interp_funcs) if dload_interp_funcs else 0 # intensity at left‐top corner
|
|
811
|
+
qR = sum(func(x_r) for func in dload_interp_funcs) if dload_interp_funcs else 0 # intensity at right‐top corner
|
|
812
|
+
else:
|
|
813
|
+
qL = 0
|
|
814
|
+
qR = 0
|
|
815
|
+
dload, d_x, d_y = calc_dload_resultant(x_l, y_lt, x_r, y_rt, qL, qR, dl_top)
|
|
816
|
+
|
|
817
|
+
# Second distributed load
|
|
818
|
+
qC2 = sum(func(x_c) for func in dload2_interp_funcs) if dload2_interp_funcs else 0 # intensity at center
|
|
819
|
+
if qC2 > 0: # We need to check qC2 to distinguish between a linear ramp up (down) and the case where the load starts or ends on one of the sides
|
|
820
|
+
qL2 = sum(func(x_l) for func in dload2_interp_funcs) if dload2_interp_funcs else 0 # intensity at left‐top corner
|
|
821
|
+
qR2 = sum(func(x_r) for func in dload2_interp_funcs) if dload2_interp_funcs else 0 # intensity at right‐top corner
|
|
822
|
+
else:
|
|
823
|
+
qL2 = 0
|
|
824
|
+
qR2 = 0
|
|
825
|
+
dload2, d_x2, d_y2 = calc_dload_resultant(x_l, y_lt, x_r, y_rt, qL2, qR2, dl_top)
|
|
826
|
+
|
|
827
|
+
# Seismic force
|
|
828
|
+
kw = k_seismic * soil_weight
|
|
829
|
+
|
|
830
|
+
# === BEGIN : "Tension crack water force" ===
|
|
831
|
+
|
|
832
|
+
# By default, zero out t and its line‐of‐action:
|
|
833
|
+
t_force = 0.0
|
|
834
|
+
y_t_loc = 0.0
|
|
835
|
+
|
|
836
|
+
# Only nonzero for the appropriate end‐slice:
|
|
837
|
+
if tcrack_water is not None and tcrack_water > 0:
|
|
838
|
+
# Horizontal resultant of water in tension crack (triangular distribution):
|
|
839
|
+
# t = (1/2) * γ_w * (d_tc)^2
|
|
840
|
+
# Here, gamma_w is the unit weight of the crack‐water (y_w),
|
|
841
|
+
# and tcrack_water is the depth of water in the crack (d_tc).
|
|
842
|
+
t_force = 0.5 * gamma_w * (tcrack_water ** 2)
|
|
843
|
+
|
|
844
|
+
if right_facing:
|
|
845
|
+
# Right‐facing slope → water pushes on left side of the first slice (i == 0)
|
|
846
|
+
if i == 0:
|
|
847
|
+
# line of action is d_tc/3 above the bottom left corner y_lb
|
|
848
|
+
t_force = - t_force # negative because it acts to the right on free body diagram
|
|
849
|
+
y_t_loc = y_lb + (tcrack_water / 3.0)
|
|
850
|
+
else:
|
|
851
|
+
# other slices = no tension‐crack force
|
|
852
|
+
t_force = 0.0
|
|
853
|
+
y_t_loc = 0.0
|
|
854
|
+
|
|
855
|
+
else:
|
|
856
|
+
# Left‐facing slope → water pushes on right side of the last slice (i == n-1)
|
|
857
|
+
if i == (len(all_xs) - 2): # last slice index = (number_of_slices − 1)
|
|
858
|
+
# line of action is d_tc/3 above the bottom right corner y_rb
|
|
859
|
+
y_t_loc = y_rb + (tcrack_water / 3.0)
|
|
860
|
+
else:
|
|
861
|
+
t_force = 0.0
|
|
862
|
+
y_t_loc = y_rb
|
|
863
|
+
# === END: "Tension crack water force" ===
|
|
864
|
+
|
|
865
|
+
# === BEGIN : "Reinforcement lines" ===
|
|
866
|
+
|
|
867
|
+
# 1) Build this slice's base as a LineString from (x_l, y_lb) to (x_r, y_rb):
|
|
868
|
+
slice_base = LineString([(x_l, y_lb), (x_r, y_rb)])
|
|
869
|
+
|
|
870
|
+
# 2) For each reinforcement line, check a single‐point intersection:
|
|
871
|
+
p_sum = 0.0
|
|
872
|
+
for rl in reinf_lines_data:
|
|
873
|
+
intersec = slice_base.intersection(rl["geom"])
|
|
874
|
+
if intersec.is_empty:
|
|
875
|
+
continue
|
|
876
|
+
|
|
877
|
+
# Since we guarantee only one intersection point, it must be a Point:
|
|
878
|
+
if isinstance(intersec, Point):
|
|
879
|
+
xi = intersec.x
|
|
880
|
+
# interpolated T at xi
|
|
881
|
+
t_i = np.interp(xi, rl["xs"], rl["ts"], left=0.0, right=0.0)
|
|
882
|
+
p_sum += t_i
|
|
883
|
+
else:
|
|
884
|
+
# (In the extremely unlikely case that intersection is not a Point,
|
|
885
|
+
# skip it. Our assumption is only one Point per slice-base.)
|
|
886
|
+
continue
|
|
887
|
+
|
|
888
|
+
# Now p_sum is the TOTAL T‐pull acting at this slice's base.
|
|
889
|
+
# === END: "Tension crack water force" ===
|
|
890
|
+
|
|
891
|
+
# Process piezometric line and pore pressures using pre-computed coordinates
|
|
892
|
+
piezo_y = piezo_y_all[i]
|
|
893
|
+
piezo_y2 = piezo_y2_all[i]
|
|
894
|
+
|
|
895
|
+
hw = 0
|
|
896
|
+
hw2 = 0
|
|
897
|
+
u = 0
|
|
898
|
+
u2 = 0
|
|
899
|
+
# Determine pore pressure method from material property
|
|
900
|
+
mat_u = materials[base_material_idx]['u'] if base_material_idx is not None else 'none'
|
|
901
|
+
if mat_u == 'none':
|
|
902
|
+
u = 0
|
|
903
|
+
u2 = 0
|
|
904
|
+
elif mat_u == 'piezo':
|
|
905
|
+
if not np.isnan(piezo_y) and piezo_y > y_cb:
|
|
906
|
+
hw = piezo_y - y_cb
|
|
907
|
+
if not np.isnan(piezo_y2) and piezo_y2 > y_cb:
|
|
908
|
+
hw2 = piezo_y2 - y_cb
|
|
909
|
+
u = hw * gamma_w if not np.isnan(piezo_y) else 0
|
|
910
|
+
u2 = hw2 * gamma_w if not np.isnan(piezo_y2) else 0
|
|
911
|
+
elif mat_u == 'seep':
|
|
912
|
+
# Seepage-based pore pressure calculation using mesh interpolation
|
|
913
|
+
if 'seep_mesh' in data and 'seep_u' in data:
|
|
914
|
+
seep_mesh = data['seep_mesh']
|
|
915
|
+
seep_u = data['seep_u']
|
|
916
|
+
|
|
917
|
+
# Interpolate pore pressure at the slice center base point
|
|
918
|
+
point = (x_c, y_cb)
|
|
919
|
+
u = interpolate_at_point(
|
|
920
|
+
seep_mesh['nodes'],
|
|
921
|
+
seep_mesh['elements'],
|
|
922
|
+
seep_mesh['element_types'],
|
|
923
|
+
seep_u,
|
|
924
|
+
point
|
|
925
|
+
)
|
|
926
|
+
else:
|
|
927
|
+
u = 0
|
|
928
|
+
|
|
929
|
+
# Check for second seepage solution (rapid drawdown)
|
|
930
|
+
if 'seep_u2' in data:
|
|
931
|
+
seep_mesh = data['seep_mesh']
|
|
932
|
+
seep_u2 = data['seep_u2']
|
|
933
|
+
|
|
934
|
+
# Interpolate pore pressure at the slice center base point
|
|
935
|
+
point = (x_c, y_cb)
|
|
936
|
+
u2 = interpolate_at_point(
|
|
937
|
+
seep_mesh['nodes'],
|
|
938
|
+
seep_mesh['elements'],
|
|
939
|
+
seep_mesh['element_types'],
|
|
940
|
+
seep_u2,
|
|
941
|
+
point
|
|
942
|
+
)
|
|
943
|
+
else:
|
|
944
|
+
u2 = 0
|
|
945
|
+
else:
|
|
946
|
+
u = 0
|
|
947
|
+
u2 = 0
|
|
948
|
+
|
|
949
|
+
# Calculate alpha (slope angle of the failure surface) more efficiently
|
|
950
|
+
delta = 0.01
|
|
951
|
+
if circular:
|
|
952
|
+
# For circular failure surface, use parametric equation for derivative
|
|
953
|
+
# The slope at any point on the circle is: dy/dx = (x - Xo) / sqrt(R^2 - (x - Xo)^2)
|
|
954
|
+
dx_circle = x_c - Xo
|
|
955
|
+
if abs(dx_circle) < R: # Check if point is on the circle
|
|
956
|
+
alpha = degrees(atan(dx_circle / sqrt(R**2 - dx_circle**2)))
|
|
957
|
+
else:
|
|
958
|
+
# Fallback to numerical method
|
|
959
|
+
y1 = get_circular_y_coordinates([x_c - delta], Xo, Yo, R)[0]
|
|
960
|
+
y2 = get_circular_y_coordinates([x_c + delta], Xo, Yo, R)[0]
|
|
961
|
+
alpha = degrees(atan2(y2 - y1, 2 * delta))
|
|
962
|
+
else:
|
|
963
|
+
# For non-circular failure surface, use geometric intersection
|
|
964
|
+
failure_line = clipped_surface
|
|
965
|
+
y1 = get_y_from_intersection(
|
|
966
|
+
failure_line.intersection(LineString([(x_c - delta, -1e6), (x_c - delta, 1e6)])))
|
|
967
|
+
y2 = get_y_from_intersection(
|
|
968
|
+
failure_line.intersection(LineString([(x_c + delta, -1e6), (x_c + delta, 1e6)])))
|
|
969
|
+
if y1 is not None and y2 is not None:
|
|
970
|
+
alpha = degrees(atan2(y2 - y1, 2 * delta))
|
|
971
|
+
else:
|
|
972
|
+
alpha = 0
|
|
973
|
+
|
|
974
|
+
if right_facing:
|
|
975
|
+
alpha = -alpha
|
|
976
|
+
dl = dx / cos(radians(alpha))
|
|
977
|
+
|
|
978
|
+
if base_material_idx is None:
|
|
979
|
+
phi = 0
|
|
980
|
+
c = 0
|
|
981
|
+
c1 = 0 # not used in rapid drawdown, but must be defined
|
|
982
|
+
phi1 = 0 # not used in rapid drawdown, but must be defined
|
|
983
|
+
d = 0 # not used in rapid drawdown, but must be defined
|
|
984
|
+
psi = 0 # not used in rapid drawdown, but must be defined
|
|
985
|
+
else:
|
|
986
|
+
if materials[base_material_idx]['option'] == 'mc':
|
|
987
|
+
c = materials[base_material_idx]['c']
|
|
988
|
+
phi = materials[base_material_idx]['phi']
|
|
989
|
+
c1 = c # make a copy for use in rapid drawdown
|
|
990
|
+
phi1 = phi # make a copy for use in rapid drawdown
|
|
991
|
+
d = materials[base_material_idx]['d']
|
|
992
|
+
psi = materials[base_material_idx]['psi']
|
|
993
|
+
else:
|
|
994
|
+
c = (materials[base_material_idx]['r_elev'] - y_cb) * materials[base_material_idx]['cp']
|
|
995
|
+
phi = 0
|
|
996
|
+
c1 = 0 # not used in rapid drawdown, but must be defined
|
|
997
|
+
phi1 = 0 # not used in rapid drawdown, but must be defined
|
|
998
|
+
d = 0 # not used in rapid drawdown, but must be defined
|
|
999
|
+
psi = 0 # not used in rapid drawdown, but must be defined
|
|
1000
|
+
|
|
1001
|
+
# Prepare slice data with conditional circle parameters
|
|
1002
|
+
slice_data = {
|
|
1003
|
+
'slice #': i + 1, # Slice numbering starts at 1
|
|
1004
|
+
'x_l': x_l, # left x-coordinate of the slice
|
|
1005
|
+
'y_lb': y_lb, # left y-coordinate of the slice base
|
|
1006
|
+
'y_lt': y_lt, # left y-coordinate of the slice top
|
|
1007
|
+
'x_r': x_r, # right x-coordinate of the slice
|
|
1008
|
+
'y_rb': y_rb, # right y-coordinate of the slice base
|
|
1009
|
+
'y_rt': y_rt, # right y-coordinate of the slice top
|
|
1010
|
+
'x_c': x_c, # center x-coordinate of the slice
|
|
1011
|
+
'y_cb': y_cb, # center y-coordinate of the slice base
|
|
1012
|
+
'y_ct': y_ct, # center y-coordinate of the slice top
|
|
1013
|
+
'y_cg': y_cg, # center of gravity y-coordinate of the slice
|
|
1014
|
+
'dx': dx, # width of the slice
|
|
1015
|
+
'alpha': alpha, # slope angle of the bottom of the slice in degrees
|
|
1016
|
+
'dl': dl, # length of the slice along the failure surface
|
|
1017
|
+
**{f'h{j+1}': h for j, h in enumerate(heights)}, # heights of each layer in the slice
|
|
1018
|
+
'w': soil_weight, # weight of the slice
|
|
1019
|
+
'qL': qL, # distributed load intensity at left edge
|
|
1020
|
+
'qR': qR, # distributed load intensity at right edge
|
|
1021
|
+
'dload': dload, # distributed load resultant (area of trapezoid)
|
|
1022
|
+
'd_x': d_x, # dist load resultant x-coordinate (point d)
|
|
1023
|
+
'd_y': d_y, # dist load resultant y-coordinate (point d)
|
|
1024
|
+
'qL2': qL2, # second distributed load intensity at left edge
|
|
1025
|
+
'qR2': qR2, # second distributed load intensity at right edge
|
|
1026
|
+
'dload2': dload2, # second distributed load resultant (area of trapezoid)
|
|
1027
|
+
'd_x2': d_x2, # second dist load resultant x-coordinate (point d)
|
|
1028
|
+
'd_y2': d_y2, # second dist load resultant y-coordinate (point d)
|
|
1029
|
+
'beta': beta, # slope angle of the top edge in degrees
|
|
1030
|
+
'kw': kw, # seismic force
|
|
1031
|
+
't': t_force, # tension crack water force
|
|
1032
|
+
'y_t': y_t_loc, # y-coordinate of the tension crack water force line of action
|
|
1033
|
+
'p': p_sum, # sum of reinforcement line T values that intersect base of slice.
|
|
1034
|
+
'n_eff': 0, # Placeholder for effective normal force
|
|
1035
|
+
'z': 0, # Placeholder for interslice side forces
|
|
1036
|
+
'theta': 0, # Placeholder for interslice angles
|
|
1037
|
+
'piezo_y': piezo_y, # y-coordinate of the piezometric surface at x_c
|
|
1038
|
+
'piezo_y2': piezo_y2, # y-coordinate of the piezometric surface at x_c for second piezometric line (rapid drawdown)
|
|
1039
|
+
'hw': hw, # height of water at x_c
|
|
1040
|
+
'u': u, # pore pressure at x_c
|
|
1041
|
+
'hw2': hw2, # height of water at x_c for second piezometric line (rapid drawdown)
|
|
1042
|
+
'u2': u2, # pore pressure at x_c for second piezometric line (rapid drawdown)
|
|
1043
|
+
'mat': base_material_idx + 1 if base_material_idx is not None else None, # index of the base material (1-indexed)
|
|
1044
|
+
'c': c, # cohesion of the base material
|
|
1045
|
+
'phi': phi, # friction angle of the base material in degrees
|
|
1046
|
+
'c1': c1, # cohesion of the base material for rapid drawdown
|
|
1047
|
+
'phi1': phi1, # friction angle of the base material for rapid drawdown
|
|
1048
|
+
'd': d, # d cohesion of the base material for rapid drawdown
|
|
1049
|
+
'psi': psi, # psi friction angle of the base material for rapid drawdown
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
# Add circle parameters only for circular failure surfaces
|
|
1053
|
+
if circular:
|
|
1054
|
+
slice_data.update({
|
|
1055
|
+
'r': R, # radius of the circular failure surface
|
|
1056
|
+
'xo': Xo, # x-coordinate of the center of the circular failure surface
|
|
1057
|
+
'yo': Yo, # y-coordinate of the center of the circular failure surface
|
|
1058
|
+
})
|
|
1059
|
+
else:
|
|
1060
|
+
slice_data.update({
|
|
1061
|
+
'r': None, # not applicable for non-circular failure surface
|
|
1062
|
+
'xo': None, # not applicable for non-circular failure surface
|
|
1063
|
+
'yo': None, # not applicable for non-circular failure surface
|
|
1064
|
+
})
|
|
1065
|
+
slices.append(slice_data)
|
|
1066
|
+
|
|
1067
|
+
df = pd.DataFrame(slices)
|
|
1068
|
+
|
|
1069
|
+
# Slice data were built by iterating from left to right. Flip the order slice data for right-facing slopes.
|
|
1070
|
+
# Slice 1 should be at the bottom and slice n at the top. This makes the slice data consistent with the
|
|
1071
|
+
# sign convention for alpha and the free-body diagram used to calculate forces.
|
|
1072
|
+
# if right_facing:
|
|
1073
|
+
# df = df.iloc[::-1].reset_index(drop=True)
|
|
1074
|
+
|
|
1075
|
+
return True, (df, clipped_surface)
|