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/plot.py
ADDED
|
@@ -0,0 +1,1484 @@
|
|
|
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
|
+
import matplotlib.pyplot as plt
|
|
16
|
+
import numpy as np
|
|
17
|
+
from matplotlib.lines import Line2D
|
|
18
|
+
from matplotlib.path import Path
|
|
19
|
+
from shapely.geometry import LineString
|
|
20
|
+
|
|
21
|
+
from .slice import generate_failure_surface
|
|
22
|
+
|
|
23
|
+
# Configure matplotlib for better text rendering
|
|
24
|
+
plt.rcParams.update({
|
|
25
|
+
"text.usetex": False,
|
|
26
|
+
"font.family": "sans-serif",
|
|
27
|
+
"font.size": 10
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
# Consistent color for materials (Tableau tab10)
|
|
31
|
+
def get_material_color(idx):
|
|
32
|
+
tableau_colors = plt.get_cmap('tab10').colors # 10 distinct colors
|
|
33
|
+
return tableau_colors[idx % len(tableau_colors)]
|
|
34
|
+
|
|
35
|
+
def get_dload_legend_handler():
|
|
36
|
+
"""
|
|
37
|
+
Creates and returns a custom legend entry for distributed loads.
|
|
38
|
+
Returns a tuple of (handler_class, dummy_patch) for use in matplotlib legends.
|
|
39
|
+
"""
|
|
40
|
+
# Create a line with built-in arrow marker
|
|
41
|
+
dummy_line = Line2D([0.0, 1.0], [0, 0], # Two points to define line
|
|
42
|
+
color='purple',
|
|
43
|
+
alpha=0.7,
|
|
44
|
+
linewidth=2,
|
|
45
|
+
marker='>', # Built-in right arrow marker
|
|
46
|
+
markersize=6, # Smaller marker size
|
|
47
|
+
markerfacecolor='purple',
|
|
48
|
+
markeredgecolor='purple',
|
|
49
|
+
drawstyle='steps-post', # Draw line then marker
|
|
50
|
+
solid_capstyle='butt')
|
|
51
|
+
|
|
52
|
+
return None, dummy_line
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def plot_profile_lines(ax, profile_lines):
|
|
56
|
+
"""
|
|
57
|
+
Plots the profile lines for each material in the slope.
|
|
58
|
+
|
|
59
|
+
Parameters:
|
|
60
|
+
ax: matplotlib Axes object
|
|
61
|
+
profile_lines: List of line coordinates representing material boundaries
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
None
|
|
65
|
+
"""
|
|
66
|
+
for i, line in enumerate(profile_lines):
|
|
67
|
+
xs, ys = zip(*line)
|
|
68
|
+
ax.plot(xs, ys, color=get_material_color(i), linewidth=1, label=f'Profile {i+1}')
|
|
69
|
+
|
|
70
|
+
def plot_max_depth(ax, profile_lines, max_depth):
|
|
71
|
+
"""
|
|
72
|
+
Plots a horizontal line representing the maximum depth limit with hash marks.
|
|
73
|
+
|
|
74
|
+
Parameters:
|
|
75
|
+
ax: matplotlib Axes object
|
|
76
|
+
profile_lines: List of line coordinates representing material boundaries
|
|
77
|
+
max_depth: Maximum allowed depth for analysis
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
None
|
|
81
|
+
"""
|
|
82
|
+
if max_depth is None:
|
|
83
|
+
return
|
|
84
|
+
x_vals = [x for line in profile_lines for x, _ in line]
|
|
85
|
+
x_min = min(x_vals)
|
|
86
|
+
x_max = max(x_vals)
|
|
87
|
+
ax.hlines(max_depth, x_min, x_max, colors='black', linewidth=1.5, label='Max Depth')
|
|
88
|
+
|
|
89
|
+
spacing = 5
|
|
90
|
+
length = 4
|
|
91
|
+
angle_rad = np.radians(60)
|
|
92
|
+
dx = length * np.cos(angle_rad)
|
|
93
|
+
dy = length * np.sin(angle_rad)
|
|
94
|
+
x_hashes = np.arange(x_min, x_max, spacing)[1:]
|
|
95
|
+
for x in x_hashes:
|
|
96
|
+
ax.plot([x, x - dx], [max_depth, max_depth - dy], color='black', linewidth=1)
|
|
97
|
+
|
|
98
|
+
def plot_failure_surface(ax, failure_surface):
|
|
99
|
+
"""
|
|
100
|
+
Plots the failure surface as a black line.
|
|
101
|
+
|
|
102
|
+
Parameters:
|
|
103
|
+
ax: matplotlib Axes object
|
|
104
|
+
failure_surface: Shapely LineString representing the failure surface
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
None
|
|
108
|
+
"""
|
|
109
|
+
if failure_surface:
|
|
110
|
+
x_clip, y_clip = zip(*failure_surface.coords)
|
|
111
|
+
ax.plot(x_clip, y_clip, 'k-', linewidth=2, label="Failure Surface")
|
|
112
|
+
|
|
113
|
+
def plot_slices(ax, slice_df, fill=True):
|
|
114
|
+
"""
|
|
115
|
+
Plots the slices used in the analysis.
|
|
116
|
+
|
|
117
|
+
Parameters:
|
|
118
|
+
ax: matplotlib Axes object
|
|
119
|
+
slice_df: DataFrame containing slice data
|
|
120
|
+
fill: Boolean indicating whether to fill the slices with color
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
None
|
|
124
|
+
"""
|
|
125
|
+
if slice_df is not None:
|
|
126
|
+
for _, row in slice_df.iterrows():
|
|
127
|
+
if fill:
|
|
128
|
+
xs = [row['x_l'], row['x_l'], row['x_r'], row['x_r'], row['x_l']]
|
|
129
|
+
ys = [row['y_lb'], row['y_lt'], row['y_rt'], row['y_rb'], row['y_lb']]
|
|
130
|
+
ax.plot(xs, ys, 'r-')
|
|
131
|
+
ax.fill(xs, ys, color='red', alpha=0.1)
|
|
132
|
+
else:
|
|
133
|
+
ax.plot([row['x_l'], row['x_l']], [row['y_lb'], row['y_lt']], 'k-', linewidth=0.5)
|
|
134
|
+
ax.plot([row['x_r'], row['x_r']], [row['y_rb'], row['y_rt']], 'k-', linewidth=0.5)
|
|
135
|
+
|
|
136
|
+
def plot_slice_numbers(ax, slice_df):
|
|
137
|
+
"""
|
|
138
|
+
Plots the slice number in the middle of each slice at the middle height.
|
|
139
|
+
Numbers are 1-indexed.
|
|
140
|
+
|
|
141
|
+
Parameters:
|
|
142
|
+
ax: matplotlib Axes object
|
|
143
|
+
slice_df: DataFrame containing slice data
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
None
|
|
147
|
+
"""
|
|
148
|
+
if slice_df is not None:
|
|
149
|
+
for _, row in slice_df.iterrows():
|
|
150
|
+
# Calculate middle x-coordinate of the slice
|
|
151
|
+
x_middle = row['x_c']
|
|
152
|
+
|
|
153
|
+
# Calculate middle height of the slice
|
|
154
|
+
y_middle = (row['y_cb'] + row['y_ct']) / 2
|
|
155
|
+
|
|
156
|
+
# Plot the slice number (1-indexed)
|
|
157
|
+
slice_number = int(row['slice #'])
|
|
158
|
+
ax.text(x_middle, y_middle, str(slice_number),
|
|
159
|
+
ha='center', va='center', fontsize=8, fontweight='bold',
|
|
160
|
+
bbox=dict(boxstyle="round,pad=0.2", facecolor='white', alpha=0.8))
|
|
161
|
+
|
|
162
|
+
def plot_piezo_line(ax, slope_data):
|
|
163
|
+
"""
|
|
164
|
+
Plots the piezometric line(s) with markers at their midpoints.
|
|
165
|
+
|
|
166
|
+
Parameters:
|
|
167
|
+
ax: matplotlib Axes object
|
|
168
|
+
data: Dictionary containing plot data with 'piezo_line' and optionally 'piezo_line2'
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
None
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
def plot_single_piezo_line(ax, piezo_line, color, label):
|
|
175
|
+
"""Internal function to plot a single piezometric line"""
|
|
176
|
+
if not piezo_line:
|
|
177
|
+
return
|
|
178
|
+
|
|
179
|
+
piezo_xs, piezo_ys = zip(*piezo_line)
|
|
180
|
+
ax.plot(piezo_xs, piezo_ys, color=color, linewidth=2, label=label)
|
|
181
|
+
|
|
182
|
+
# Find middle x-coordinate and corresponding y value
|
|
183
|
+
x_min, x_max = min(piezo_xs), max(piezo_xs)
|
|
184
|
+
mid_x = (x_min + x_max) / 2
|
|
185
|
+
|
|
186
|
+
# Interpolate y value at mid_x
|
|
187
|
+
from scipy.interpolate import interp1d
|
|
188
|
+
if len(piezo_xs) > 1:
|
|
189
|
+
f = interp1d(piezo_xs, piezo_ys, kind='linear', bounds_error=False, fill_value='extrapolate')
|
|
190
|
+
mid_y = f(mid_x)
|
|
191
|
+
ax.plot(mid_x, mid_y + 6, marker='v', color=color, markersize=8)
|
|
192
|
+
|
|
193
|
+
# Plot both piezometric lines
|
|
194
|
+
plot_single_piezo_line(ax, slope_data.get('piezo_line'), 'b', "Piezometric Line")
|
|
195
|
+
plot_single_piezo_line(ax, slope_data.get('piezo_line2'), 'skyblue', "Piezometric Line 2")
|
|
196
|
+
|
|
197
|
+
def plot_tcrack_surface(ax, tcrack_surface):
|
|
198
|
+
"""
|
|
199
|
+
Plots the tension crack surface as a thin dashed red line.
|
|
200
|
+
|
|
201
|
+
Parameters:
|
|
202
|
+
ax: matplotlib Axes object
|
|
203
|
+
tcrack_surface: Shapely LineString
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
None
|
|
207
|
+
"""
|
|
208
|
+
if tcrack_surface is None:
|
|
209
|
+
return
|
|
210
|
+
|
|
211
|
+
x_vals, y_vals = tcrack_surface.xy
|
|
212
|
+
ax.plot(x_vals, y_vals, linestyle='--', color='red', linewidth=1.0, label='Tension Crack Depth')
|
|
213
|
+
|
|
214
|
+
def plot_dloads(ax, slope_data):
|
|
215
|
+
"""
|
|
216
|
+
Plots distributed loads as arrows along the surface.
|
|
217
|
+
"""
|
|
218
|
+
gamma_w = slope_data['gamma_water']
|
|
219
|
+
ground_surface = slope_data['ground_surface']
|
|
220
|
+
|
|
221
|
+
def plot_single_dload_set(ax, dloads, color, label):
|
|
222
|
+
"""Internal function to plot a single set of distributed loads"""
|
|
223
|
+
if not dloads:
|
|
224
|
+
return
|
|
225
|
+
|
|
226
|
+
# find the max horizontal length of the ground surface
|
|
227
|
+
max_horizontal_length_ground = 0
|
|
228
|
+
for pt in ground_surface.coords:
|
|
229
|
+
max_horizontal_length_ground = max(max_horizontal_length_ground, pt[0])
|
|
230
|
+
|
|
231
|
+
arrow_spacing = max_horizontal_length_ground / 60
|
|
232
|
+
|
|
233
|
+
# find the max dload value
|
|
234
|
+
max_dload = 0
|
|
235
|
+
for line in dloads:
|
|
236
|
+
max_dload = max(max_dload, max(pt['Normal'] for pt in line))
|
|
237
|
+
|
|
238
|
+
arrow_height = max_dload / gamma_w
|
|
239
|
+
head_length = arrow_height / 12
|
|
240
|
+
head_width = head_length * 0.8
|
|
241
|
+
|
|
242
|
+
# Find the maximum load value for scaling
|
|
243
|
+
max_load = 0
|
|
244
|
+
for line in dloads:
|
|
245
|
+
max_load = max(max_load, max(pt['Normal'] for pt in line))
|
|
246
|
+
|
|
247
|
+
for line in dloads:
|
|
248
|
+
if len(line) < 2:
|
|
249
|
+
continue
|
|
250
|
+
|
|
251
|
+
xs = [pt['X'] for pt in line]
|
|
252
|
+
ys = [pt['Y'] for pt in line]
|
|
253
|
+
ns = [pt['Normal'] for pt in line]
|
|
254
|
+
|
|
255
|
+
# Process line segments
|
|
256
|
+
for i in range(len(line) - 1):
|
|
257
|
+
x1, y1, n1 = xs[i], ys[i], ns[i]
|
|
258
|
+
x2, y2, n2 = xs[i+1], ys[i+1], ns[i+1]
|
|
259
|
+
|
|
260
|
+
# Calculate segment direction (perpendicular to this segment)
|
|
261
|
+
dx = x2 - x1
|
|
262
|
+
dy = y2 - y1
|
|
263
|
+
segment_length = np.sqrt(dx**2 + dy**2)
|
|
264
|
+
|
|
265
|
+
if segment_length == 0:
|
|
266
|
+
continue
|
|
267
|
+
|
|
268
|
+
# Normalize the segment direction
|
|
269
|
+
dx_norm = dx / segment_length
|
|
270
|
+
dy_norm = dy / segment_length
|
|
271
|
+
|
|
272
|
+
# Perpendicular direction (rotate 90 degrees CCW)
|
|
273
|
+
perp_dx = -dy_norm
|
|
274
|
+
perp_dy = dx_norm
|
|
275
|
+
|
|
276
|
+
# Generate arrows along this segment
|
|
277
|
+
dx_abs = abs(x2 - x1)
|
|
278
|
+
num_arrows = max(1, int(round(dx_abs / arrow_spacing)))
|
|
279
|
+
if dx_abs == 0:
|
|
280
|
+
t_values = np.array([0.0, 1.0])
|
|
281
|
+
else:
|
|
282
|
+
t_values = np.linspace(0, 1, num_arrows + 1)
|
|
283
|
+
|
|
284
|
+
# Store arrow top points for connecting line
|
|
285
|
+
top_xs = []
|
|
286
|
+
top_ys = []
|
|
287
|
+
|
|
288
|
+
# Add start point if it's the first segment and load is zero
|
|
289
|
+
if i == 0 and n1 == 0:
|
|
290
|
+
top_xs.append(x1)
|
|
291
|
+
top_ys.append(y1)
|
|
292
|
+
|
|
293
|
+
for t in t_values:
|
|
294
|
+
# Interpolate position along segment
|
|
295
|
+
x = x1 + t * dx
|
|
296
|
+
y = y1 + t * dy
|
|
297
|
+
|
|
298
|
+
# Interpolate load value
|
|
299
|
+
n = n1 + t * (n2 - n1)
|
|
300
|
+
|
|
301
|
+
# Scale arrow height based on equivalent water depth
|
|
302
|
+
if max_load > 0:
|
|
303
|
+
water_depth = n / gamma_w
|
|
304
|
+
arrow_height = water_depth # Direct water depth, not scaled relative to max
|
|
305
|
+
else:
|
|
306
|
+
arrow_height = 0
|
|
307
|
+
|
|
308
|
+
# For very small arrows, just store surface point for connecting line
|
|
309
|
+
if arrow_height < 0.5:
|
|
310
|
+
top_xs.append(x)
|
|
311
|
+
top_ys.append(y)
|
|
312
|
+
continue
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
# Calculate arrow start point (above surface)
|
|
316
|
+
arrow_start_x = x + perp_dx * arrow_height
|
|
317
|
+
arrow_start_y = y + perp_dy * arrow_height
|
|
318
|
+
|
|
319
|
+
# Store points for connecting line
|
|
320
|
+
top_xs.append(arrow_start_x)
|
|
321
|
+
top_ys.append(arrow_start_y)
|
|
322
|
+
|
|
323
|
+
# Draw arrow - extend all the way to surface point
|
|
324
|
+
arrow_length = np.sqrt((x - arrow_start_x)**2 + (y - arrow_start_y)**2)
|
|
325
|
+
if head_length > arrow_length:
|
|
326
|
+
# Draw a simple line without arrowhead
|
|
327
|
+
ax.plot([arrow_start_x, x], [arrow_start_y, y],
|
|
328
|
+
color=color, linewidth=2, alpha=0.7)
|
|
329
|
+
else:
|
|
330
|
+
# Draw arrow with head
|
|
331
|
+
ax.arrow(arrow_start_x, arrow_start_y,
|
|
332
|
+
x - arrow_start_x, y - arrow_start_y,
|
|
333
|
+
head_width=head_width, head_length=head_length,
|
|
334
|
+
fc=color, ec=color, alpha=0.7,
|
|
335
|
+
length_includes_head=True)
|
|
336
|
+
|
|
337
|
+
# Add end point if it's the last segment and load is zero
|
|
338
|
+
if i == len(line) - 2 and n2 == 0:
|
|
339
|
+
top_xs.append(x2)
|
|
340
|
+
top_ys.append(y2)
|
|
341
|
+
|
|
342
|
+
# Draw connecting line at arrow tops
|
|
343
|
+
if top_xs:
|
|
344
|
+
ax.plot(top_xs, top_ys, color=color, linewidth=1.5, alpha=0.8)
|
|
345
|
+
|
|
346
|
+
# Draw the surface line itself
|
|
347
|
+
ax.plot(xs, ys, color=color, linewidth=1.5, alpha=0.8, label=label)
|
|
348
|
+
|
|
349
|
+
dloads = slope_data['dloads']
|
|
350
|
+
dloads2 = slope_data.get('dloads2', [])
|
|
351
|
+
plot_single_dload_set(ax, dloads, 'purple', 'Distributed Load')
|
|
352
|
+
plot_single_dload_set(ax, dloads2, 'orange', 'Distributed Load 2')
|
|
353
|
+
|
|
354
|
+
def plot_circles(ax, slope_data):
|
|
355
|
+
"""
|
|
356
|
+
Plots starting circles with center markers and arrows.
|
|
357
|
+
|
|
358
|
+
Parameters:
|
|
359
|
+
ax (matplotlib axis): The plotting axis
|
|
360
|
+
slope_data (dict): Slope data dictionary containing circles
|
|
361
|
+
|
|
362
|
+
Returns:
|
|
363
|
+
None
|
|
364
|
+
"""
|
|
365
|
+
circles = slope_data['circles']
|
|
366
|
+
for circle in circles:
|
|
367
|
+
Xo = circle['Xo']
|
|
368
|
+
Yo = circle['Yo']
|
|
369
|
+
R = circle['R']
|
|
370
|
+
# theta = np.linspace(0, 2 * np.pi, 100)
|
|
371
|
+
# x_circle = Xo + R * np.cos(theta)
|
|
372
|
+
# y_circle = Yo + R * np.sin(theta)
|
|
373
|
+
# ax.plot(x_circle, y_circle, 'r--', label='Circle')
|
|
374
|
+
|
|
375
|
+
# Plot the portion of the circle in the slope
|
|
376
|
+
ground_surface = slope_data['ground_surface']
|
|
377
|
+
success, result = generate_failure_surface(ground_surface, circular=True, circle=circle)
|
|
378
|
+
if not success:
|
|
379
|
+
continue # or handle error
|
|
380
|
+
# result = (x_min, x_max, y_left, y_right, clipped_surface)
|
|
381
|
+
x_min, x_max, y_left, y_right, clipped_surface = result
|
|
382
|
+
x_clip, y_clip = zip(*clipped_surface.coords)
|
|
383
|
+
ax.plot(x_clip, y_clip, 'r--', label="Circle")
|
|
384
|
+
|
|
385
|
+
# Center marker
|
|
386
|
+
ax.plot(Xo, Yo, 'r+', markersize=10)
|
|
387
|
+
|
|
388
|
+
# Arrow direction: point from center to midpoint of failure surface
|
|
389
|
+
mid_idx = len(x_clip) // 2
|
|
390
|
+
x_mid = x_clip[mid_idx]
|
|
391
|
+
y_mid = y_clip[mid_idx]
|
|
392
|
+
|
|
393
|
+
dx = x_mid - Xo
|
|
394
|
+
dy = y_mid - Yo
|
|
395
|
+
|
|
396
|
+
# Normalize direction vector
|
|
397
|
+
length = np.hypot(dx, dy)
|
|
398
|
+
if length != 0:
|
|
399
|
+
dx /= length
|
|
400
|
+
dy /= length
|
|
401
|
+
|
|
402
|
+
# Shorten shaft length slightly
|
|
403
|
+
shaft_length = R - 5
|
|
404
|
+
|
|
405
|
+
ax.arrow(Xo, Yo, dx * shaft_length, dy * shaft_length,
|
|
406
|
+
head_width=5, head_length=5, fc='red', ec='red')
|
|
407
|
+
|
|
408
|
+
def plot_non_circ(ax, non_circ):
|
|
409
|
+
"""
|
|
410
|
+
Plots a non-circular failure surface.
|
|
411
|
+
|
|
412
|
+
Parameters:
|
|
413
|
+
ax: matplotlib Axes object
|
|
414
|
+
non_circ: List of coordinates representing the non-circular failure surface
|
|
415
|
+
|
|
416
|
+
Returns:
|
|
417
|
+
None
|
|
418
|
+
"""
|
|
419
|
+
xs, ys = zip(*non_circ)
|
|
420
|
+
ax.plot(xs, ys, 'r--', label='Non-Circular Surface')
|
|
421
|
+
|
|
422
|
+
def plot_material_table(ax, materials, xloc=0.6, yloc=0.7):
|
|
423
|
+
"""
|
|
424
|
+
Adds a material properties table to the plot.
|
|
425
|
+
|
|
426
|
+
Parameters:
|
|
427
|
+
ax: matplotlib Axes object
|
|
428
|
+
materials: List of material property dictionaries
|
|
429
|
+
xloc: x-location of table (0-1)
|
|
430
|
+
yloc: y-location of table (0-1)
|
|
431
|
+
|
|
432
|
+
Returns:
|
|
433
|
+
None
|
|
434
|
+
"""
|
|
435
|
+
if not materials:
|
|
436
|
+
return
|
|
437
|
+
|
|
438
|
+
# Check if any materials have non-zero d and psi values
|
|
439
|
+
has_d_psi = any(mat.get('d', 0) > 0 or mat.get('psi', 0) > 0 for mat in materials)
|
|
440
|
+
|
|
441
|
+
# Check material options
|
|
442
|
+
options = set(mat['option'] for mat in materials)
|
|
443
|
+
|
|
444
|
+
# Decide column headers
|
|
445
|
+
if options == {'mc'}:
|
|
446
|
+
if has_d_psi:
|
|
447
|
+
col_labels = ["Mat", "Name", "γ", "c", "φ", "d", "ψ"]
|
|
448
|
+
else:
|
|
449
|
+
col_labels = ["Mat", "Name", "γ", "c", "φ"]
|
|
450
|
+
elif options == {'cp'}:
|
|
451
|
+
if has_d_psi:
|
|
452
|
+
col_labels = ["Mat", "Name", "γ", "cp", "rₑ", "d", "ψ"]
|
|
453
|
+
else:
|
|
454
|
+
col_labels = ["Mat", "Name", "γ", "cp", "rₑ"]
|
|
455
|
+
else:
|
|
456
|
+
if has_d_psi:
|
|
457
|
+
col_labels = ["Mat", "Name", "γ", "c / cp", "φ / rₑ", "d", "ψ"]
|
|
458
|
+
else:
|
|
459
|
+
col_labels = ["Mat", "Name", "γ", "c / cp", "φ / rₑ"]
|
|
460
|
+
|
|
461
|
+
# Build table rows
|
|
462
|
+
table_data = []
|
|
463
|
+
for idx, mat in enumerate(materials):
|
|
464
|
+
name = mat['name']
|
|
465
|
+
gamma = mat['gamma']
|
|
466
|
+
option = mat['option']
|
|
467
|
+
|
|
468
|
+
if option == 'mc':
|
|
469
|
+
c = mat['c']
|
|
470
|
+
phi = mat['phi']
|
|
471
|
+
if has_d_psi:
|
|
472
|
+
d = mat.get('d', 0)
|
|
473
|
+
psi = mat.get('psi', 0)
|
|
474
|
+
d_str = f"{d:.1f}" if d > 0 or psi > 0 else "-"
|
|
475
|
+
psi_str = f"{psi:.1f}" if d > 0 or psi > 0 else "-"
|
|
476
|
+
row = [idx+1, name, f"{gamma:.1f}", f"{c:.1f}", f"{phi:.1f}", d_str, psi_str]
|
|
477
|
+
else:
|
|
478
|
+
row = [idx+1, name, f"{gamma:.1f}", f"{c:.1f}", f"{phi:.1f}"]
|
|
479
|
+
elif option == 'cp':
|
|
480
|
+
cp = mat['cp']
|
|
481
|
+
r_elev = mat['r_elev']
|
|
482
|
+
if has_d_psi:
|
|
483
|
+
d = mat.get('d', 0)
|
|
484
|
+
psi = mat.get('psi', 0)
|
|
485
|
+
d_str = f"{d:.1f}" if d > 0 or psi > 0 else "-"
|
|
486
|
+
psi_str = f"{psi:.1f}" if d > 0 or psi > 0 else "-"
|
|
487
|
+
row = [idx+1, name, f"{gamma:.1f}", f"{cp:.2f}", f"{r_elev:.1f}", d_str, psi_str]
|
|
488
|
+
else:
|
|
489
|
+
row = [idx+1, name, f"{gamma:.1f}", f"{cp:.2f}", f"{r_elev:.1f}"]
|
|
490
|
+
else:
|
|
491
|
+
if has_d_psi:
|
|
492
|
+
d = mat.get('d', 0)
|
|
493
|
+
psi = mat.get('psi', 0)
|
|
494
|
+
d_str = f"{d:.1f}" if d > 0 or psi > 0 else "-"
|
|
495
|
+
psi_str = f"{psi:.1f}" if d > 0 or psi > 0 else "-"
|
|
496
|
+
row = [idx+1, name, f"{gamma:.1f}", "-", "-", d_str, psi_str]
|
|
497
|
+
else:
|
|
498
|
+
row = [idx+1, name, f"{gamma:.1f}", "-", "-"]
|
|
499
|
+
table_data.append(row)
|
|
500
|
+
|
|
501
|
+
# Adjust table width based on number of columns
|
|
502
|
+
table_width = 0.25 if has_d_psi else 0.2
|
|
503
|
+
|
|
504
|
+
# Add the table
|
|
505
|
+
table = ax.table(cellText=table_data,
|
|
506
|
+
colLabels=col_labels,
|
|
507
|
+
loc='upper right',
|
|
508
|
+
colLoc='center',
|
|
509
|
+
cellLoc='center',
|
|
510
|
+
bbox=[xloc, yloc, table_width, 0.25])
|
|
511
|
+
table.auto_set_font_size(False)
|
|
512
|
+
table.set_fontsize(8)
|
|
513
|
+
|
|
514
|
+
def plot_base_stresses(ax, slice_df, scale_frac=0.5, alpha=0.3):
|
|
515
|
+
"""
|
|
516
|
+
Plots base normal stresses for each slice as bars.
|
|
517
|
+
|
|
518
|
+
Parameters:
|
|
519
|
+
ax: matplotlib Axes object
|
|
520
|
+
slice_df: DataFrame containing slice data
|
|
521
|
+
scale_frac: Fraction of plot height for bar scaling
|
|
522
|
+
alpha: Transparency for bars
|
|
523
|
+
|
|
524
|
+
Returns:
|
|
525
|
+
None
|
|
526
|
+
"""
|
|
527
|
+
u = slice_df['u'].values
|
|
528
|
+
n_eff = slice_df['n_eff'].values
|
|
529
|
+
dl = slice_df['dl'].values
|
|
530
|
+
heights = slice_df['y_ct'] - slice_df['y_cb']
|
|
531
|
+
max_ht = heights.max() if not heights.empty else 1.0
|
|
532
|
+
max_bar_len = max_ht * scale_frac
|
|
533
|
+
|
|
534
|
+
max_stress = np.max(np.abs(n_eff)) if len(n_eff) > 0 else 1.0
|
|
535
|
+
max_u = np.max(u) if len(u) > 0 else 1.0
|
|
536
|
+
|
|
537
|
+
for i, (index, row) in enumerate(slice_df.iterrows()):
|
|
538
|
+
if i >= len(n_eff):
|
|
539
|
+
break
|
|
540
|
+
|
|
541
|
+
x1, y1 = row['x_l'], row['y_lb']
|
|
542
|
+
x2, y2 = row['x_r'], row['y_rb']
|
|
543
|
+
|
|
544
|
+
stress = n_eff[i]
|
|
545
|
+
pore = u[i]
|
|
546
|
+
|
|
547
|
+
dx = x2 - x1
|
|
548
|
+
dy = y2 - y1
|
|
549
|
+
length = np.hypot(dx, dy)
|
|
550
|
+
if length == 0:
|
|
551
|
+
continue
|
|
552
|
+
|
|
553
|
+
nx = -dy / length
|
|
554
|
+
ny = dx / length
|
|
555
|
+
|
|
556
|
+
# --- Normal stress trapezoid ---
|
|
557
|
+
bar_len = (abs(stress) / max_stress) * max_bar_len
|
|
558
|
+
direction = -np.sign(stress)
|
|
559
|
+
|
|
560
|
+
x1_top = x1 + direction * bar_len * nx
|
|
561
|
+
y1_top = y1 + direction * bar_len * ny
|
|
562
|
+
x2_top = x2 + direction * bar_len * nx
|
|
563
|
+
y2_top = y2 + direction * bar_len * ny
|
|
564
|
+
|
|
565
|
+
poly_x = [x1, x2, x2_top, x1_top]
|
|
566
|
+
poly_y = [y1, y2, y2_top, y1_top]
|
|
567
|
+
|
|
568
|
+
ax.fill(poly_x, poly_y, facecolor='none', edgecolor='red' if stress <= 0 else 'limegreen', hatch='.....',
|
|
569
|
+
linewidth=1)
|
|
570
|
+
|
|
571
|
+
# --- Pore pressure trapezoid ---
|
|
572
|
+
u_len = (pore / max_stress) * max_bar_len
|
|
573
|
+
u_dir = -1 # always into the base
|
|
574
|
+
|
|
575
|
+
ux1_top = x1 + u_dir * u_len * nx
|
|
576
|
+
uy1_top = y1 + u_dir * u_len * ny
|
|
577
|
+
ux2_top = x2 + u_dir * u_len * nx
|
|
578
|
+
uy2_top = y2 + u_dir * u_len * ny
|
|
579
|
+
|
|
580
|
+
poly_ux = [x1, x2, ux2_top, ux1_top]
|
|
581
|
+
poly_uy = [y1, y2, uy2_top, uy1_top]
|
|
582
|
+
|
|
583
|
+
ax.fill(poly_ux, poly_uy, color='blue', alpha=alpha, edgecolor='k', linewidth=1)
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def plot_thrust_line_from_df(ax, slice_df,
|
|
587
|
+
color: str = 'red',
|
|
588
|
+
linestyle: str = '--',
|
|
589
|
+
linewidth: float = 1,
|
|
590
|
+
label: str = 'Line of Thrust'):
|
|
591
|
+
"""
|
|
592
|
+
Plots the line of thrust from the slice dataframe.
|
|
593
|
+
|
|
594
|
+
Parameters:
|
|
595
|
+
ax: matplotlib Axes object
|
|
596
|
+
slice_df: DataFrame containing slice data with 'yt_l' and 'yt_r' columns
|
|
597
|
+
color: Color of the line
|
|
598
|
+
linestyle: Style of the line
|
|
599
|
+
linewidth: Width of the line
|
|
600
|
+
label: Label for the line in the legend
|
|
601
|
+
|
|
602
|
+
Returns:
|
|
603
|
+
None
|
|
604
|
+
"""
|
|
605
|
+
# Check if required columns exist
|
|
606
|
+
if 'yt_l' not in slice_df.columns or 'yt_r' not in slice_df.columns:
|
|
607
|
+
return
|
|
608
|
+
|
|
609
|
+
# Create thrust line coordinates from slice data
|
|
610
|
+
thrust_xs = []
|
|
611
|
+
thrust_ys = []
|
|
612
|
+
|
|
613
|
+
for _, row in slice_df.iterrows():
|
|
614
|
+
# Add left point of current slice
|
|
615
|
+
thrust_xs.append(row['x_l'])
|
|
616
|
+
thrust_ys.append(row['yt_l'])
|
|
617
|
+
|
|
618
|
+
# Add right point of current slice (same as left point of next slice)
|
|
619
|
+
thrust_xs.append(row['x_r'])
|
|
620
|
+
thrust_ys.append(row['yt_r'])
|
|
621
|
+
|
|
622
|
+
# Plot the thrust line
|
|
623
|
+
ax.plot(thrust_xs, thrust_ys,
|
|
624
|
+
color=color,
|
|
625
|
+
linestyle=linestyle,
|
|
626
|
+
linewidth=linewidth,
|
|
627
|
+
label=label)
|
|
628
|
+
|
|
629
|
+
def compute_ylim(data, slice_df, scale_frac=0.5, pad_fraction=0.1):
|
|
630
|
+
"""
|
|
631
|
+
Computes y-limits for plotting based on slice data.
|
|
632
|
+
|
|
633
|
+
Parameters:
|
|
634
|
+
data: Input data
|
|
635
|
+
slice_df: pandas.DataFrame with slice data, must have 'y_lt' and 'y_lb' for stress‐bar sizing
|
|
636
|
+
scale_frac: fraction of max slice height used when drawing stress bars
|
|
637
|
+
pad_fraction: fraction of total range to pad above/below finally
|
|
638
|
+
|
|
639
|
+
Returns:
|
|
640
|
+
(y_min, y_max) suitable for ax.set_ylim(...)
|
|
641
|
+
"""
|
|
642
|
+
import numpy as np
|
|
643
|
+
|
|
644
|
+
y_vals = []
|
|
645
|
+
|
|
646
|
+
# 1) collect all profile line elevations
|
|
647
|
+
for line in data.get('profile_lines', []):
|
|
648
|
+
if hasattr(line, "xy"):
|
|
649
|
+
_, ys = line.xy
|
|
650
|
+
else:
|
|
651
|
+
_, ys = zip(*line)
|
|
652
|
+
y_vals.extend(ys)
|
|
653
|
+
|
|
654
|
+
# 2) explicitly include the deepest allowed depth
|
|
655
|
+
if "max_depth" in data and data["max_depth"] is not None:
|
|
656
|
+
y_vals.append(data["max_depth"])
|
|
657
|
+
|
|
658
|
+
if not y_vals:
|
|
659
|
+
return 0.0, 1.0
|
|
660
|
+
|
|
661
|
+
y_min = min(y_vals)
|
|
662
|
+
y_max = max(y_vals)
|
|
663
|
+
|
|
664
|
+
# 3) ensure the largest stress bar will fit
|
|
665
|
+
# stress‐bar length = scale_frac * slice height
|
|
666
|
+
heights = slice_df["y_lt"] - slice_df["y_lb"]
|
|
667
|
+
if not heights.empty:
|
|
668
|
+
max_bar = heights.max() * scale_frac
|
|
669
|
+
y_min -= max_bar
|
|
670
|
+
y_max += max_bar
|
|
671
|
+
|
|
672
|
+
# 4) add a final small pad
|
|
673
|
+
pad = (y_max - y_min) * pad_fraction
|
|
674
|
+
return y_min - pad, y_max + pad
|
|
675
|
+
|
|
676
|
+
# ========== FOR PLOTTING INPUT DATA =========
|
|
677
|
+
|
|
678
|
+
def plot_reinforcement_lines(ax, slope_data):
|
|
679
|
+
"""
|
|
680
|
+
Plots the reinforcement lines from slope_data.
|
|
681
|
+
|
|
682
|
+
Parameters:
|
|
683
|
+
ax: matplotlib Axes object
|
|
684
|
+
slope_data: Dictionary containing slope data with 'reinforce_lines' key
|
|
685
|
+
|
|
686
|
+
Returns:
|
|
687
|
+
None
|
|
688
|
+
"""
|
|
689
|
+
if 'reinforce_lines' not in slope_data or not slope_data['reinforce_lines']:
|
|
690
|
+
return
|
|
691
|
+
|
|
692
|
+
tension_points_plotted = False # Track if tension points have been added to legend
|
|
693
|
+
|
|
694
|
+
for i, line in enumerate(slope_data['reinforce_lines']):
|
|
695
|
+
# Extract x and y coordinates from the line points
|
|
696
|
+
xs = [point['X'] for point in line]
|
|
697
|
+
ys = [point['Y'] for point in line]
|
|
698
|
+
|
|
699
|
+
# Plot the reinforcement line with a distinctive style
|
|
700
|
+
ax.plot(xs, ys, color='darkgray', linewidth=3, linestyle='-',
|
|
701
|
+
alpha=0.8, label='Reinforcement Line' if i == 0 else "")
|
|
702
|
+
|
|
703
|
+
# Add markers at each point to show tension values
|
|
704
|
+
for j, point in enumerate(line):
|
|
705
|
+
tension = point.get('T', 0.0)
|
|
706
|
+
if tension > 0:
|
|
707
|
+
# Use smaller marker size proportional to tension (normalized)
|
|
708
|
+
max_tension = max(p.get('T', 0.0) for p in line)
|
|
709
|
+
marker_size = 10 + 15 * (tension / max_tension) if max_tension > 0 else 10
|
|
710
|
+
ax.scatter(point['X'], point['Y'], s=marker_size,
|
|
711
|
+
color='red', alpha=0.7, zorder=5,
|
|
712
|
+
label='Tension Points' if not tension_points_plotted else "")
|
|
713
|
+
tension_points_plotted = True
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
def plot_inputs(slope_data, title="Slope Geometry and Inputs", width=12, height=6, mat_table=True):
|
|
717
|
+
"""
|
|
718
|
+
Creates a plot showing the slope geometry and input parameters.
|
|
719
|
+
|
|
720
|
+
Parameters:
|
|
721
|
+
slope_data: Dictionary containing plot data
|
|
722
|
+
title: Title for the plot
|
|
723
|
+
width: Width of the plot in inches
|
|
724
|
+
height: Height of the plot in inches
|
|
725
|
+
mat_table: Controls material table display. Can be:
|
|
726
|
+
- True: Auto-position material table to avoid overlaps
|
|
727
|
+
- False: Don't show material table
|
|
728
|
+
- 'auto': Auto-position material table to avoid overlaps
|
|
729
|
+
- String: Specific location for material table ('upper left', 'upper right', 'upper center',
|
|
730
|
+
'lower left', 'lower right', 'lower center', 'center left', 'center right', 'center')
|
|
731
|
+
|
|
732
|
+
Returns:
|
|
733
|
+
None
|
|
734
|
+
"""
|
|
735
|
+
fig, ax = plt.subplots(figsize=(width, height))
|
|
736
|
+
|
|
737
|
+
# Plot contents
|
|
738
|
+
plot_profile_lines(ax, slope_data['profile_lines'])
|
|
739
|
+
plot_max_depth(ax, slope_data['profile_lines'], slope_data['max_depth'])
|
|
740
|
+
plot_piezo_line(ax, slope_data)
|
|
741
|
+
plot_dloads(ax, slope_data)
|
|
742
|
+
plot_tcrack_surface(ax, slope_data['tcrack_surface'])
|
|
743
|
+
plot_reinforcement_lines(ax, slope_data)
|
|
744
|
+
|
|
745
|
+
if slope_data['circular']:
|
|
746
|
+
plot_circles(ax, slope_data)
|
|
747
|
+
else:
|
|
748
|
+
plot_non_circ(ax, slope_data['non_circ'])
|
|
749
|
+
|
|
750
|
+
# Handle material table display
|
|
751
|
+
if mat_table:
|
|
752
|
+
if isinstance(mat_table, str) and mat_table != 'auto':
|
|
753
|
+
# Convert location string to xloc, yloc coordinates (inside plot area with margins)
|
|
754
|
+
location_map = {
|
|
755
|
+
'upper left': (0.05, 0.70),
|
|
756
|
+
'upper right': (0.70, 0.70),
|
|
757
|
+
'upper center': (0.35, 0.70),
|
|
758
|
+
'lower left': (0.05, 0.05),
|
|
759
|
+
'lower right': (0.70, 0.05),
|
|
760
|
+
'lower center': (0.35, 0.05),
|
|
761
|
+
'center left': (0.05, 0.35),
|
|
762
|
+
'center right': (0.70, 0.35),
|
|
763
|
+
'center': (0.35, 0.35)
|
|
764
|
+
}
|
|
765
|
+
if mat_table in location_map:
|
|
766
|
+
xloc, yloc = location_map[mat_table]
|
|
767
|
+
plot_material_table(ax, slope_data['materials'], xloc=xloc, yloc=yloc)
|
|
768
|
+
else:
|
|
769
|
+
# Default to upper right if invalid location
|
|
770
|
+
plot_material_table(ax, slope_data['materials'], xloc=0.75, yloc=0.75)
|
|
771
|
+
else:
|
|
772
|
+
# Auto-position or default: find best location
|
|
773
|
+
plot_elements_bounds = get_plot_elements_bounds(ax, slope_data)
|
|
774
|
+
xloc, yloc = find_best_table_position(ax, slope_data['materials'], plot_elements_bounds)
|
|
775
|
+
plot_material_table(ax, slope_data['materials'], xloc=xloc, yloc=yloc)
|
|
776
|
+
|
|
777
|
+
ax.set_aspect('equal') # ✅ Equal aspect
|
|
778
|
+
ax.set_xlabel("x")
|
|
779
|
+
ax.set_ylabel("y")
|
|
780
|
+
ax.grid(False)
|
|
781
|
+
|
|
782
|
+
# Get legend handles and labels
|
|
783
|
+
handles, labels = ax.get_legend_handles_labels()
|
|
784
|
+
|
|
785
|
+
# Add distributed load to legend if present
|
|
786
|
+
if slope_data['dloads']:
|
|
787
|
+
handler_class, dummy_line = get_dload_legend_handler()
|
|
788
|
+
handles.append(dummy_line)
|
|
789
|
+
labels.append('Distributed Load')
|
|
790
|
+
|
|
791
|
+
ax.legend(
|
|
792
|
+
handles=handles,
|
|
793
|
+
labels=labels,
|
|
794
|
+
loc='upper center',
|
|
795
|
+
bbox_to_anchor=(0.5, -0.12),
|
|
796
|
+
ncol=2
|
|
797
|
+
)
|
|
798
|
+
|
|
799
|
+
ax.set_title(title)
|
|
800
|
+
|
|
801
|
+
plt.tight_layout()
|
|
802
|
+
plt.show()
|
|
803
|
+
|
|
804
|
+
# ========== Main Plotting Function =========
|
|
805
|
+
|
|
806
|
+
def plot_solution(slope_data, slice_df, failure_surface, results, width=12, height=7, slice_numbers=False):
|
|
807
|
+
"""
|
|
808
|
+
Plots the full solution including slices, numbers, thrust line, and base stresses.
|
|
809
|
+
|
|
810
|
+
Parameters:
|
|
811
|
+
data: Input data
|
|
812
|
+
slice_df: DataFrame containing slice data
|
|
813
|
+
failure_surface: Failure surface geometry
|
|
814
|
+
results: Solution results
|
|
815
|
+
width: Width of the plot in inches
|
|
816
|
+
height: Height of the plot in inches
|
|
817
|
+
|
|
818
|
+
Returns:
|
|
819
|
+
None
|
|
820
|
+
"""
|
|
821
|
+
fig, ax = plt.subplots(figsize=(width, height))
|
|
822
|
+
ax.set_xlabel("x")
|
|
823
|
+
ax.set_ylabel("y")
|
|
824
|
+
ax.grid(False)
|
|
825
|
+
|
|
826
|
+
plot_profile_lines(ax, slope_data['profile_lines'])
|
|
827
|
+
plot_max_depth(ax, slope_data['profile_lines'], slope_data['max_depth'])
|
|
828
|
+
plot_slices(ax, slice_df, fill=False)
|
|
829
|
+
plot_failure_surface(ax, failure_surface)
|
|
830
|
+
plot_piezo_line(ax, slope_data)
|
|
831
|
+
plot_dloads(ax, slope_data)
|
|
832
|
+
plot_tcrack_surface(ax, slope_data['tcrack_surface'])
|
|
833
|
+
plot_reinforcement_lines(ax, slope_data)
|
|
834
|
+
if slice_numbers:
|
|
835
|
+
plot_slice_numbers(ax, slice_df)
|
|
836
|
+
# plot_material_table(ax, data['materials'], xloc=0.75) # Adjust this so that it fits with the legend
|
|
837
|
+
|
|
838
|
+
alpha = 0.3
|
|
839
|
+
if results['method'] == 'spencer':
|
|
840
|
+
plot_thrust_line_from_df(ax, slice_df)
|
|
841
|
+
|
|
842
|
+
plot_base_stresses(ax, slice_df, alpha=alpha)
|
|
843
|
+
|
|
844
|
+
import matplotlib.patches as mpatches
|
|
845
|
+
normal_patch = mpatches.Patch(facecolor='none', edgecolor='green', hatch='.....', label="Eff Normal Stress (σ')")
|
|
846
|
+
pore_patch = mpatches.Patch(color='blue', alpha=alpha, label='Pore Pressure (u)')
|
|
847
|
+
|
|
848
|
+
# Get legend handles and labels
|
|
849
|
+
handles, labels = ax.get_legend_handles_labels()
|
|
850
|
+
handles.extend([normal_patch, pore_patch])
|
|
851
|
+
labels.extend(["Eff Normal Stress (σ')", 'Pore Pressure (u)'])
|
|
852
|
+
|
|
853
|
+
# Add distributed load to legend if present
|
|
854
|
+
if slope_data['dloads']:
|
|
855
|
+
handler_class, dummy_line = get_dload_legend_handler()
|
|
856
|
+
handles.append(dummy_line)
|
|
857
|
+
labels.append('Distributed Load')
|
|
858
|
+
|
|
859
|
+
ax.legend(
|
|
860
|
+
handles=handles,
|
|
861
|
+
labels=labels,
|
|
862
|
+
loc='upper center',
|
|
863
|
+
bbox_to_anchor=(0.5, -0.15),
|
|
864
|
+
ncol=3
|
|
865
|
+
)
|
|
866
|
+
|
|
867
|
+
# Add vertical space below for the legend
|
|
868
|
+
plt.subplots_adjust(bottom=0.2)
|
|
869
|
+
ax.set_aspect('equal')
|
|
870
|
+
|
|
871
|
+
fs = results['FS']
|
|
872
|
+
method = results['method']
|
|
873
|
+
if method == 'oms':
|
|
874
|
+
title = f'OMS: FS = {fs:.3f}'
|
|
875
|
+
elif method == 'bishop':
|
|
876
|
+
title = f'Bishop: FS = {fs:.3f}'
|
|
877
|
+
elif method == 'spencer':
|
|
878
|
+
theta = results['theta']
|
|
879
|
+
title = f'Spencer: FS = {fs:.3f}, θ = {theta:.2f}°'
|
|
880
|
+
elif method == 'janbu':
|
|
881
|
+
fo = results['fo']
|
|
882
|
+
title = f'Janbu-Corrected: FS = {fs:.3f}, fo = {fo:.2f}'
|
|
883
|
+
elif method == 'corps_engineers':
|
|
884
|
+
theta = results['theta']
|
|
885
|
+
title = f'Corps Engineers: FS = {fs:.3f}, θ = {theta:.2f}°'
|
|
886
|
+
elif method == 'lowe_karafiath':
|
|
887
|
+
title = f'Lowe & Karafiath: FS = {fs:.3f}'
|
|
888
|
+
ax.set_title(title)
|
|
889
|
+
|
|
890
|
+
# zoom y‐axis to just cover the slope and depth, with a little breathing room (thrust line can be outside)
|
|
891
|
+
ymin, ymax = compute_ylim(slope_data, slice_df, pad_fraction=0.05)
|
|
892
|
+
ax.set_ylim(ymin, ymax)
|
|
893
|
+
|
|
894
|
+
plt.tight_layout()
|
|
895
|
+
plt.show()
|
|
896
|
+
|
|
897
|
+
# ========== Functions for Search Results =========
|
|
898
|
+
|
|
899
|
+
def plot_failure_surfaces(ax, fs_cache):
|
|
900
|
+
"""
|
|
901
|
+
Plots all failure surfaces from the factor of safety cache.
|
|
902
|
+
|
|
903
|
+
Parameters:
|
|
904
|
+
ax: matplotlib Axes object
|
|
905
|
+
fs_cache: List of dictionaries containing failure surface data and FS values
|
|
906
|
+
|
|
907
|
+
Returns:
|
|
908
|
+
None
|
|
909
|
+
"""
|
|
910
|
+
for i, result in reversed(list(enumerate(fs_cache))):
|
|
911
|
+
surface = result['failure_surface']
|
|
912
|
+
if surface is None or surface.is_empty:
|
|
913
|
+
continue
|
|
914
|
+
x, y = zip(*surface.coords)
|
|
915
|
+
color = 'red' if i == 0 else 'gray'
|
|
916
|
+
lw = 2 if i == 0 else 1
|
|
917
|
+
ax.plot(x, y, color=color, linestyle='-', linewidth=lw, alpha=1.0 if i == 0 else 0.6)
|
|
918
|
+
|
|
919
|
+
def plot_circle_centers(ax, fs_cache):
|
|
920
|
+
"""
|
|
921
|
+
Plots the centers of circular failure surfaces.
|
|
922
|
+
|
|
923
|
+
Parameters:
|
|
924
|
+
ax: matplotlib Axes object
|
|
925
|
+
fs_cache: List of dictionaries containing circle center data
|
|
926
|
+
|
|
927
|
+
Returns:
|
|
928
|
+
None
|
|
929
|
+
"""
|
|
930
|
+
for result in fs_cache:
|
|
931
|
+
ax.plot(result['Xo'], result['Yo'], 'ko', markersize=3, alpha=0.6)
|
|
932
|
+
|
|
933
|
+
def plot_search_path(ax, search_path):
|
|
934
|
+
"""
|
|
935
|
+
Plots the search path used to find the critical failure surface.
|
|
936
|
+
|
|
937
|
+
Parameters:
|
|
938
|
+
ax: matplotlib Axes object
|
|
939
|
+
search_path: List of dictionaries containing search path coordinates
|
|
940
|
+
|
|
941
|
+
Returns:
|
|
942
|
+
None
|
|
943
|
+
"""
|
|
944
|
+
if len(search_path) < 2:
|
|
945
|
+
return # need at least two points to draw an arrow
|
|
946
|
+
|
|
947
|
+
for i in range(len(search_path) - 1):
|
|
948
|
+
start = search_path[i]
|
|
949
|
+
end = search_path[i + 1]
|
|
950
|
+
dx = end['x'] - start['x']
|
|
951
|
+
dy = end['y'] - start['y']
|
|
952
|
+
ax.arrow(start['x'], start['y'], dx, dy,
|
|
953
|
+
head_width=1, head_length=2, fc='green', ec='green', length_includes_head=True)
|
|
954
|
+
|
|
955
|
+
def plot_circular_search_results(slope_data, fs_cache, search_path=None, highlight_fs=True, width=12, height=7):
|
|
956
|
+
"""
|
|
957
|
+
Creates a plot showing the results of a circular failure surface search.
|
|
958
|
+
|
|
959
|
+
Parameters:
|
|
960
|
+
slope_data: Dictionary containing plot data
|
|
961
|
+
fs_cache: List of dictionaries containing failure surface data and FS values
|
|
962
|
+
search_path: List of dictionaries containing search path coordinates
|
|
963
|
+
highlight_fs: Boolean indicating whether to highlight the critical failure surface
|
|
964
|
+
width: Width of the plot in inches
|
|
965
|
+
height: Height of the plot in inches
|
|
966
|
+
|
|
967
|
+
Returns:
|
|
968
|
+
None
|
|
969
|
+
"""
|
|
970
|
+
fig, ax = plt.subplots(figsize=(width, height))
|
|
971
|
+
|
|
972
|
+
plot_profile_lines(ax, slope_data['profile_lines'])
|
|
973
|
+
plot_max_depth(ax, slope_data['profile_lines'], slope_data['max_depth'])
|
|
974
|
+
plot_piezo_line(ax, slope_data)
|
|
975
|
+
plot_dloads(ax, slope_data)
|
|
976
|
+
plot_tcrack_surface(ax, slope_data['tcrack_surface'])
|
|
977
|
+
|
|
978
|
+
plot_failure_surfaces(ax, fs_cache)
|
|
979
|
+
plot_circle_centers(ax, fs_cache)
|
|
980
|
+
if search_path:
|
|
981
|
+
plot_search_path(ax, search_path)
|
|
982
|
+
|
|
983
|
+
ax.set_aspect('equal')
|
|
984
|
+
ax.set_xlabel("x")
|
|
985
|
+
ax.set_ylabel("y")
|
|
986
|
+
ax.grid(False)
|
|
987
|
+
ax.legend()
|
|
988
|
+
|
|
989
|
+
if highlight_fs and fs_cache:
|
|
990
|
+
critical_fs = fs_cache[0]['FS']
|
|
991
|
+
ax.set_title(f"Critical Factor of Safety = {critical_fs:.3f}")
|
|
992
|
+
|
|
993
|
+
plt.tight_layout()
|
|
994
|
+
plt.show()
|
|
995
|
+
|
|
996
|
+
def plot_noncircular_search_results(slope_data, fs_cache, search_path=None, highlight_fs=True, width=12, height=7):
|
|
997
|
+
"""
|
|
998
|
+
Creates a plot showing the results of a non-circular failure surface search.
|
|
999
|
+
|
|
1000
|
+
Parameters:
|
|
1001
|
+
slope_data: Dictionary containing plot data
|
|
1002
|
+
fs_cache: List of dictionaries containing failure surface data and FS values
|
|
1003
|
+
search_path: List of dictionaries containing search path coordinates
|
|
1004
|
+
highlight_fs: Boolean indicating whether to highlight the critical failure surface
|
|
1005
|
+
width: Width of the plot in inches
|
|
1006
|
+
height: Height of the plot in inches
|
|
1007
|
+
|
|
1008
|
+
Returns:
|
|
1009
|
+
None
|
|
1010
|
+
"""
|
|
1011
|
+
fig, ax = plt.subplots(figsize=(width, height))
|
|
1012
|
+
|
|
1013
|
+
# Plot basic profile elements
|
|
1014
|
+
plot_profile_lines(ax, slope_data['profile_lines'])
|
|
1015
|
+
plot_max_depth(ax, slope_data['profile_lines'], slope_data['max_depth'])
|
|
1016
|
+
plot_piezo_line(ax, slope_data)
|
|
1017
|
+
plot_dloads(ax, slope_data)
|
|
1018
|
+
plot_tcrack_surface(ax, slope_data['tcrack_surface'])
|
|
1019
|
+
|
|
1020
|
+
# Plot all failure surfaces from cache
|
|
1021
|
+
for i, result in reversed(list(enumerate(fs_cache))):
|
|
1022
|
+
surface = result['failure_surface']
|
|
1023
|
+
if surface is None or surface.is_empty:
|
|
1024
|
+
continue
|
|
1025
|
+
x, y = zip(*surface.coords)
|
|
1026
|
+
color = 'red' if i == 0 else 'gray'
|
|
1027
|
+
lw = 2 if i == 0 else 1
|
|
1028
|
+
ax.plot(x, y, color=color, linestyle='-', linewidth=lw, alpha=1.0 if i == 0 else 0.6)
|
|
1029
|
+
|
|
1030
|
+
# Plot search path if provided
|
|
1031
|
+
if search_path:
|
|
1032
|
+
for i in range(len(search_path) - 1):
|
|
1033
|
+
start = search_path[i]
|
|
1034
|
+
end = search_path[i + 1]
|
|
1035
|
+
# For non-circular search, we need to plot the movement of each point
|
|
1036
|
+
start_points = np.array(start['points'])
|
|
1037
|
+
end_points = np.array(end['points'])
|
|
1038
|
+
|
|
1039
|
+
# Plot arrows for each moving point
|
|
1040
|
+
for j in range(len(start_points)):
|
|
1041
|
+
dx = end_points[j, 0] - start_points[j, 0]
|
|
1042
|
+
dy = end_points[j, 1] - start_points[j, 1]
|
|
1043
|
+
if abs(dx) > 1e-6 or abs(dy) > 1e-6: # Only plot if point moved
|
|
1044
|
+
ax.arrow(start_points[j, 0], start_points[j, 1], dx, dy,
|
|
1045
|
+
head_width=1, head_length=2, fc='green', ec='green',
|
|
1046
|
+
length_includes_head=True, alpha=0.6)
|
|
1047
|
+
|
|
1048
|
+
ax.set_aspect('equal')
|
|
1049
|
+
ax.set_xlabel("x")
|
|
1050
|
+
ax.set_ylabel("y")
|
|
1051
|
+
ax.grid(False)
|
|
1052
|
+
ax.legend()
|
|
1053
|
+
|
|
1054
|
+
if highlight_fs and fs_cache:
|
|
1055
|
+
critical_fs = fs_cache[0]['FS']
|
|
1056
|
+
ax.set_title(f"Critical Factor of Safety = {critical_fs:.3f}")
|
|
1057
|
+
|
|
1058
|
+
plt.tight_layout()
|
|
1059
|
+
plt.show()
|
|
1060
|
+
|
|
1061
|
+
def plot_reliability_results(slope_data, reliability_data, width=12, height=7):
|
|
1062
|
+
"""
|
|
1063
|
+
Creates a plot showing the results of reliability analysis.
|
|
1064
|
+
|
|
1065
|
+
Parameters:
|
|
1066
|
+
slope_data: Dictionary containing plot data
|
|
1067
|
+
reliability_data: Dictionary containing reliability analysis results
|
|
1068
|
+
width: Width of the plot in inches
|
|
1069
|
+
height: Height of the plot in inches
|
|
1070
|
+
|
|
1071
|
+
Returns:
|
|
1072
|
+
None
|
|
1073
|
+
"""
|
|
1074
|
+
fig, ax = plt.subplots(figsize=(width, height))
|
|
1075
|
+
|
|
1076
|
+
# Plot basic slope elements (same as other search functions)
|
|
1077
|
+
plot_profile_lines(ax, slope_data['profile_lines'])
|
|
1078
|
+
plot_max_depth(ax, slope_data['profile_lines'], slope_data['max_depth'])
|
|
1079
|
+
plot_piezo_line(ax, slope_data)
|
|
1080
|
+
plot_dloads(ax, slope_data)
|
|
1081
|
+
plot_tcrack_surface(ax, slope_data['tcrack_surface'])
|
|
1082
|
+
|
|
1083
|
+
# Plot reliability-specific failure surfaces
|
|
1084
|
+
fs_cache = reliability_data['fs_cache']
|
|
1085
|
+
|
|
1086
|
+
# Plot all failure surfaces
|
|
1087
|
+
for i, fs_data in enumerate(fs_cache):
|
|
1088
|
+
result = fs_data['result']
|
|
1089
|
+
name = fs_data['name']
|
|
1090
|
+
failure_surface = result['failure_surface']
|
|
1091
|
+
|
|
1092
|
+
# Convert failure surface to coordinates
|
|
1093
|
+
if hasattr(failure_surface, 'coords'):
|
|
1094
|
+
coords = list(failure_surface.coords)
|
|
1095
|
+
else:
|
|
1096
|
+
coords = failure_surface
|
|
1097
|
+
|
|
1098
|
+
x_coords = [pt[0] for pt in coords]
|
|
1099
|
+
y_coords = [pt[1] for pt in coords]
|
|
1100
|
+
|
|
1101
|
+
# Color and styling based on surface type
|
|
1102
|
+
if name == "MLV":
|
|
1103
|
+
# Highlight the MLV (critical) surface in red
|
|
1104
|
+
ax.plot(x_coords, y_coords, color='red', linewidth=3,
|
|
1105
|
+
label=f'$F_{{MLV}}$ Surface (FS={result["FS"]:.3f})', zorder=10)
|
|
1106
|
+
else:
|
|
1107
|
+
# Other surfaces in different colors
|
|
1108
|
+
if '+' in name:
|
|
1109
|
+
color = 'blue'
|
|
1110
|
+
alpha = 0.7
|
|
1111
|
+
label = f'$F^+$ ({name}) (FS={result["FS"]:.3f})'
|
|
1112
|
+
else: # '-' in name
|
|
1113
|
+
color = 'green'
|
|
1114
|
+
alpha = 0.7
|
|
1115
|
+
label = f'$F^-$ ({name}) (FS={result["FS"]:.3f})'
|
|
1116
|
+
|
|
1117
|
+
ax.plot(x_coords, y_coords, color=color, linewidth=1.5,
|
|
1118
|
+
alpha=alpha, label=label, zorder=5)
|
|
1119
|
+
|
|
1120
|
+
|
|
1121
|
+
|
|
1122
|
+
# Standard finalization
|
|
1123
|
+
ax.set_aspect('equal')
|
|
1124
|
+
ax.set_xlabel("x")
|
|
1125
|
+
ax.set_ylabel("y")
|
|
1126
|
+
ax.grid(False)
|
|
1127
|
+
ax.legend(bbox_to_anchor=(1.05, 1), loc='upper left')
|
|
1128
|
+
|
|
1129
|
+
# Title with reliability statistics using mathtext
|
|
1130
|
+
F_MLV = reliability_data['F_MLV']
|
|
1131
|
+
sigma_F = reliability_data['sigma_F']
|
|
1132
|
+
COV_F = reliability_data['COV_F']
|
|
1133
|
+
reliability = reliability_data['reliability']
|
|
1134
|
+
prob_failure = reliability_data['prob_failure']
|
|
1135
|
+
|
|
1136
|
+
ax.set_title(f"Reliability Analysis Results\n"
|
|
1137
|
+
f"$F_{{MLV}}$ = {F_MLV:.3f}, $\\sigma_F$ = {sigma_F:.3f}, "
|
|
1138
|
+
f"$COV_F$ = {COV_F:.3f}\n"
|
|
1139
|
+
f"Reliability = {reliability*100:.2f}%, $P_f$ = {prob_failure*100:.2f}%")
|
|
1140
|
+
|
|
1141
|
+
plt.tight_layout()
|
|
1142
|
+
plt.show()
|
|
1143
|
+
|
|
1144
|
+
def plot_mesh(mesh, materials=None, figsize=(14, 6), pad_frac=0.05, show_nodes=True, label_elements=False, label_nodes=False):
|
|
1145
|
+
"""
|
|
1146
|
+
Plot the finite element mesh with material regions.
|
|
1147
|
+
|
|
1148
|
+
Parameters:
|
|
1149
|
+
mesh: Mesh dictionary with 'nodes', 'elements', 'element_types', and 'element_materials' keys
|
|
1150
|
+
materials: Optional list of material dictionaries for legend labels
|
|
1151
|
+
figsize: Figure size tuple
|
|
1152
|
+
pad_frac: Fraction of mesh size to use for padding around plot
|
|
1153
|
+
show_nodes: If True, plot points at node locations
|
|
1154
|
+
label_elements: If True, label each element with its number at its centroid
|
|
1155
|
+
label_nodes: If True, label each node with its number
|
|
1156
|
+
"""
|
|
1157
|
+
import matplotlib.pyplot as plt
|
|
1158
|
+
from matplotlib.patches import Patch
|
|
1159
|
+
from matplotlib.collections import PolyCollection
|
|
1160
|
+
import numpy as np
|
|
1161
|
+
|
|
1162
|
+
nodes = mesh["nodes"]
|
|
1163
|
+
elements = mesh["elements"]
|
|
1164
|
+
element_types = mesh["element_types"]
|
|
1165
|
+
mat_ids = mesh["element_materials"]
|
|
1166
|
+
|
|
1167
|
+
fig, ax = plt.subplots(figsize=figsize)
|
|
1168
|
+
|
|
1169
|
+
# Group elements by material ID
|
|
1170
|
+
material_elements = {}
|
|
1171
|
+
for i, (element, elem_type, mid) in enumerate(zip(elements, element_types, mat_ids)):
|
|
1172
|
+
if mid not in material_elements:
|
|
1173
|
+
material_elements[mid] = []
|
|
1174
|
+
|
|
1175
|
+
# Only process 2D elements (skip 1D elements which have elem_type 2)
|
|
1176
|
+
if elem_type == 2: # Skip 1D elements
|
|
1177
|
+
continue
|
|
1178
|
+
|
|
1179
|
+
# Use corner nodes to define element boundary (no subdivision needed)
|
|
1180
|
+
if elem_type in [3, 6]: # Triangular elements (linear or quadratic)
|
|
1181
|
+
element_coords = [nodes[element[0]], nodes[element[1]], nodes[element[2]]]
|
|
1182
|
+
elif elem_type in [4, 8, 9]: # Quadrilateral elements (linear or quadratic)
|
|
1183
|
+
element_coords = [nodes[element[0]], nodes[element[1]], nodes[element[2]], nodes[element[3]]]
|
|
1184
|
+
else:
|
|
1185
|
+
continue # Skip unknown element types
|
|
1186
|
+
|
|
1187
|
+
material_elements[mid].append(element_coords)
|
|
1188
|
+
|
|
1189
|
+
legend_elements = []
|
|
1190
|
+
|
|
1191
|
+
# Plot 1D elements FIRST (bottom layer) if present in mesh
|
|
1192
|
+
if "elements_1d" in mesh and "element_types_1d" in mesh and "element_materials_1d" in mesh:
|
|
1193
|
+
elements_1d = mesh["elements_1d"]
|
|
1194
|
+
element_types_1d = mesh["element_types_1d"]
|
|
1195
|
+
mat_ids_1d = mesh["element_materials_1d"]
|
|
1196
|
+
|
|
1197
|
+
# Group 1D elements by material ID
|
|
1198
|
+
material_lines = {}
|
|
1199
|
+
for i, (element_1d, elem_type_1d, mid_1d) in enumerate(zip(elements_1d, element_types_1d, mat_ids_1d)):
|
|
1200
|
+
if mid_1d not in material_lines:
|
|
1201
|
+
material_lines[mid_1d] = []
|
|
1202
|
+
|
|
1203
|
+
# Get line coordinates based on actual number of nodes
|
|
1204
|
+
# elem_type_1d contains the number of nodes (2 for linear, 3 for quadratic)
|
|
1205
|
+
if elem_type_1d == 2: # Linear 1D element (2 nodes)
|
|
1206
|
+
# Skip zero-padded elements
|
|
1207
|
+
if element_1d[1] != 0: # Valid second node
|
|
1208
|
+
line_coords = [nodes[element_1d[0]], nodes[element_1d[1]]]
|
|
1209
|
+
else:
|
|
1210
|
+
continue # Skip invalid element
|
|
1211
|
+
elif elem_type_1d == 3: # Quadratic 1D element (3 nodes)
|
|
1212
|
+
# For visualization, connect all three nodes or just endpoints
|
|
1213
|
+
line_coords = [nodes[element_1d[0]], nodes[element_1d[1]], nodes[element_1d[2]]]
|
|
1214
|
+
else:
|
|
1215
|
+
continue # Skip unknown 1D element types
|
|
1216
|
+
|
|
1217
|
+
material_lines[mid_1d].append(line_coords)
|
|
1218
|
+
|
|
1219
|
+
# Plot 1D elements with distinctive style
|
|
1220
|
+
for mid_1d, lines_list in material_lines.items():
|
|
1221
|
+
for line_coords in lines_list:
|
|
1222
|
+
xs = [coord[0] for coord in line_coords]
|
|
1223
|
+
ys = [coord[1] for coord in line_coords]
|
|
1224
|
+
ax.plot(xs, ys, color='red', linewidth=3, alpha=0.8, solid_capstyle='round')
|
|
1225
|
+
|
|
1226
|
+
# Add 1D elements to legend
|
|
1227
|
+
if material_lines:
|
|
1228
|
+
legend_elements.append(plt.Line2D([0], [0], color='red', linewidth=3,
|
|
1229
|
+
alpha=0.8, label='1D Elements'))
|
|
1230
|
+
|
|
1231
|
+
# Plot 2D elements SECOND (middle layer)
|
|
1232
|
+
for mid, elements_list in material_elements.items():
|
|
1233
|
+
# Create polygon collection for this material
|
|
1234
|
+
poly_collection = PolyCollection(elements_list,
|
|
1235
|
+
facecolor=get_material_color(mid),
|
|
1236
|
+
edgecolor='k',
|
|
1237
|
+
alpha=0.4,
|
|
1238
|
+
linewidth=0.5)
|
|
1239
|
+
ax.add_collection(poly_collection)
|
|
1240
|
+
|
|
1241
|
+
# Add to legend
|
|
1242
|
+
if materials and mid <= len(materials) and materials[mid-1].get('name'):
|
|
1243
|
+
label = materials[mid-1]['name'] # Convert to 0-based indexing
|
|
1244
|
+
else:
|
|
1245
|
+
label = f'Material {mid}'
|
|
1246
|
+
|
|
1247
|
+
legend_elements.append(Patch(facecolor=get_material_color(mid),
|
|
1248
|
+
edgecolor='k',
|
|
1249
|
+
alpha=0.4,
|
|
1250
|
+
label=label))
|
|
1251
|
+
|
|
1252
|
+
# Label 2D elements if requested
|
|
1253
|
+
if label_elements:
|
|
1254
|
+
for idx, (element, element_type) in enumerate(zip(elements, element_types)):
|
|
1255
|
+
# Calculate element centroid based on element type
|
|
1256
|
+
if element_type == 3: # 3-node triangle
|
|
1257
|
+
element_coords = nodes[element[:3]]
|
|
1258
|
+
elif element_type == 6: # 6-node triangle
|
|
1259
|
+
element_coords = nodes[element[:6]]
|
|
1260
|
+
elif element_type == 4: # 4-node quad
|
|
1261
|
+
element_coords = nodes[element[:4]]
|
|
1262
|
+
elif element_type == 8: # 8-node quad
|
|
1263
|
+
element_coords = nodes[element[:8]]
|
|
1264
|
+
elif element_type == 9: # 9-node quad
|
|
1265
|
+
element_coords = nodes[element[:9]]
|
|
1266
|
+
else:
|
|
1267
|
+
continue # Skip unknown element types
|
|
1268
|
+
|
|
1269
|
+
centroid = np.mean(element_coords, axis=0)
|
|
1270
|
+
ax.text(centroid[0], centroid[1], str(idx+1),
|
|
1271
|
+
ha='center', va='center', fontsize=6, color='black', alpha=0.7,
|
|
1272
|
+
zorder=12)
|
|
1273
|
+
|
|
1274
|
+
# Label 1D elements if requested (with different color)
|
|
1275
|
+
if label_elements and "elements_1d" in mesh:
|
|
1276
|
+
elements_1d = mesh["elements_1d"]
|
|
1277
|
+
element_types_1d = mesh["element_types_1d"]
|
|
1278
|
+
|
|
1279
|
+
for idx, (element_1d, elem_type_1d) in enumerate(zip(elements_1d, element_types_1d)):
|
|
1280
|
+
# Skip zero-padded elements
|
|
1281
|
+
if elem_type_1d == 2 and element_1d[1] != 0: # Linear 1D element
|
|
1282
|
+
# Calculate midpoint of line element
|
|
1283
|
+
coord1 = nodes[element_1d[0]]
|
|
1284
|
+
coord2 = nodes[element_1d[1]]
|
|
1285
|
+
midpoint = (coord1 + coord2) / 2
|
|
1286
|
+
ax.text(midpoint[0], midpoint[1], f"1D{idx+1}",
|
|
1287
|
+
ha='center', va='center', fontsize=6, color='black', alpha=0.9,
|
|
1288
|
+
zorder=13)
|
|
1289
|
+
elif elem_type_1d == 3 and element_1d[2] != 0: # Quadratic 1D element
|
|
1290
|
+
# Use middle node as label position (if it exists)
|
|
1291
|
+
midpoint = nodes[element_1d[1]]
|
|
1292
|
+
ax.text(midpoint[0], midpoint[1], f"1D{idx+1}",
|
|
1293
|
+
ha='center', va='center', fontsize=6, color='black', alpha=0.9,
|
|
1294
|
+
zorder=13)
|
|
1295
|
+
|
|
1296
|
+
# Plot nodes LAST (top layer) if requested
|
|
1297
|
+
if show_nodes:
|
|
1298
|
+
# Plot all nodes - if meshing is correct, all nodes should be used
|
|
1299
|
+
ax.plot(nodes[:, 0], nodes[:, 1], 'k.', markersize=2)
|
|
1300
|
+
# Add to legend
|
|
1301
|
+
legend_elements.append(plt.Line2D([0], [0], marker='o', color='w',
|
|
1302
|
+
markerfacecolor='k', markersize=6,
|
|
1303
|
+
label=f'Nodes ({len(nodes)})', linestyle='None'))
|
|
1304
|
+
|
|
1305
|
+
# Label nodes if requested
|
|
1306
|
+
if label_nodes:
|
|
1307
|
+
# Label all nodes
|
|
1308
|
+
for i, (x, y) in enumerate(nodes):
|
|
1309
|
+
ax.text(x + 0.5, y + 0.5, str(i+1), fontsize=6, color='blue', alpha=0.7,
|
|
1310
|
+
ha='left', va='bottom', zorder=14)
|
|
1311
|
+
|
|
1312
|
+
ax.set_aspect('equal')
|
|
1313
|
+
ax.set_title("Finite Element Mesh with Material Regions (Triangles and Quads)")
|
|
1314
|
+
|
|
1315
|
+
# Add legend if we have materials
|
|
1316
|
+
if legend_elements:
|
|
1317
|
+
ax.legend(handles=legend_elements, loc='upper center', bbox_to_anchor=(0.5, -0.05), ncol=min(len(legend_elements), 4))
|
|
1318
|
+
|
|
1319
|
+
# Add cushion
|
|
1320
|
+
x_min, x_max = nodes[:, 0].min(), nodes[:, 0].max()
|
|
1321
|
+
y_min, y_max = nodes[:, 1].min(), nodes[:, 1].max()
|
|
1322
|
+
x_pad = (x_max - x_min) * pad_frac
|
|
1323
|
+
y_pad = (y_max - y_min) * pad_frac
|
|
1324
|
+
ax.set_xlim(x_min - x_pad, x_max + x_pad)
|
|
1325
|
+
ax.set_ylim(y_min - y_pad, y_max + y_pad)
|
|
1326
|
+
|
|
1327
|
+
# Add extra cushion for legend space
|
|
1328
|
+
ax.set_ylim(y_min - y_pad, y_max + y_pad)
|
|
1329
|
+
|
|
1330
|
+
plt.tight_layout()
|
|
1331
|
+
plt.show()
|
|
1332
|
+
|
|
1333
|
+
|
|
1334
|
+
def plot_polygons(polygons, title="Material Zone Polygons"):
|
|
1335
|
+
"""
|
|
1336
|
+
Plot all material zone polygons in a single figure.
|
|
1337
|
+
|
|
1338
|
+
Parameters:
|
|
1339
|
+
polygons: List of polygon coordinate lists
|
|
1340
|
+
title: Plot title
|
|
1341
|
+
"""
|
|
1342
|
+
import matplotlib.pyplot as plt
|
|
1343
|
+
|
|
1344
|
+
fig, ax = plt.subplots(figsize=(12, 8))
|
|
1345
|
+
for i, polygon in enumerate(polygons):
|
|
1346
|
+
xs = [x for x, y in polygon]
|
|
1347
|
+
ys = [y for x, y in polygon]
|
|
1348
|
+
ax.fill(xs, ys, color=get_material_color(i), alpha=0.6, label=f'Material {i}')
|
|
1349
|
+
ax.plot(xs, ys, color=get_material_color(i), linewidth=1)
|
|
1350
|
+
ax.set_xlabel('X Coordinate')
|
|
1351
|
+
ax.set_ylabel('Y Coordinate')
|
|
1352
|
+
ax.set_title(title)
|
|
1353
|
+
ax.legend()
|
|
1354
|
+
ax.grid(True, alpha=0.3)
|
|
1355
|
+
ax.set_aspect('equal')
|
|
1356
|
+
plt.tight_layout()
|
|
1357
|
+
plt.show()
|
|
1358
|
+
|
|
1359
|
+
|
|
1360
|
+
def plot_polygons_separately(polygons, title_prefix='Material Zone'):
|
|
1361
|
+
"""
|
|
1362
|
+
Plot each polygon in a separate matplotlib frame (subplot), with vertices as round dots.
|
|
1363
|
+
|
|
1364
|
+
Parameters:
|
|
1365
|
+
polygons: List of polygon coordinate lists
|
|
1366
|
+
title_prefix: Prefix for each subplot title
|
|
1367
|
+
"""
|
|
1368
|
+
import matplotlib.pyplot as plt
|
|
1369
|
+
|
|
1370
|
+
n = len(polygons)
|
|
1371
|
+
fig, axes = plt.subplots(n, 1, figsize=(8, 3 * n), squeeze=False)
|
|
1372
|
+
for i, polygon in enumerate(polygons):
|
|
1373
|
+
xs = [x for x, y in polygon]
|
|
1374
|
+
ys = [y for x, y in polygon]
|
|
1375
|
+
ax = axes[i, 0]
|
|
1376
|
+
ax.fill(xs, ys, color=get_material_color(i), alpha=0.6, label=f'Material {i}')
|
|
1377
|
+
ax.plot(xs, ys, color=get_material_color(i), linewidth=1)
|
|
1378
|
+
ax.scatter(xs, ys, color='k', s=30, marker='o', zorder=3, label='Vertices')
|
|
1379
|
+
ax.set_xlabel('X Coordinate')
|
|
1380
|
+
ax.set_ylabel('Y Coordinate')
|
|
1381
|
+
ax.set_title(f'{title_prefix} {i}')
|
|
1382
|
+
ax.grid(True, alpha=0.3)
|
|
1383
|
+
ax.set_aspect('equal')
|
|
1384
|
+
ax.legend()
|
|
1385
|
+
plt.tight_layout()
|
|
1386
|
+
plt.show()
|
|
1387
|
+
|
|
1388
|
+
|
|
1389
|
+
def find_best_table_position(ax, materials, plot_elements_bounds):
|
|
1390
|
+
"""
|
|
1391
|
+
Find the best position for the material table to avoid overlaps.
|
|
1392
|
+
|
|
1393
|
+
Parameters:
|
|
1394
|
+
ax: matplotlib Axes object
|
|
1395
|
+
materials: List of materials to determine table size
|
|
1396
|
+
plot_elements_bounds: List of (x_min, x_max, y_min, y_max) for existing elements
|
|
1397
|
+
|
|
1398
|
+
Returns:
|
|
1399
|
+
(xloc, yloc) coordinates for table placement
|
|
1400
|
+
"""
|
|
1401
|
+
# Calculate table size based on number of materials and columns
|
|
1402
|
+
num_materials = len(materials)
|
|
1403
|
+
has_d_psi = any(mat.get('d', 0) > 0 or mat.get('psi', 0) > 0 for mat in materials)
|
|
1404
|
+
table_height = 0.05 + 0.025 * num_materials # Height per row
|
|
1405
|
+
table_width = 0.25 if has_d_psi else 0.2
|
|
1406
|
+
|
|
1407
|
+
# Define candidate positions (priority order) - with margins from borders
|
|
1408
|
+
candidates = [
|
|
1409
|
+
(0.05, 0.70), # upper left
|
|
1410
|
+
(0.70, 0.70), # upper right
|
|
1411
|
+
(0.05, 0.05), # lower left
|
|
1412
|
+
(0.70, 0.05), # lower right
|
|
1413
|
+
(0.35, 0.70), # upper center
|
|
1414
|
+
(0.35, 0.05), # lower center
|
|
1415
|
+
(0.05, 0.35), # center left
|
|
1416
|
+
(0.70, 0.35), # center right
|
|
1417
|
+
(0.35, 0.35), # center
|
|
1418
|
+
]
|
|
1419
|
+
|
|
1420
|
+
# Check each candidate position for overlaps
|
|
1421
|
+
for xloc, yloc in candidates:
|
|
1422
|
+
table_bounds = (xloc, xloc + table_width, yloc - table_height, yloc)
|
|
1423
|
+
|
|
1424
|
+
# Check if table overlaps with any plot elements
|
|
1425
|
+
overlap = False
|
|
1426
|
+
for elem_bounds in plot_elements_bounds:
|
|
1427
|
+
elem_x_min, elem_x_max, elem_y_min, elem_y_max = elem_bounds
|
|
1428
|
+
table_x_min, table_x_max, table_y_min, table_y_max = table_bounds
|
|
1429
|
+
|
|
1430
|
+
# Check for overlap
|
|
1431
|
+
if not (table_x_max < elem_x_min or table_x_min > elem_x_max or
|
|
1432
|
+
table_y_max < elem_y_min or table_y_min > elem_y_max):
|
|
1433
|
+
overlap = True
|
|
1434
|
+
break
|
|
1435
|
+
|
|
1436
|
+
if not overlap:
|
|
1437
|
+
return xloc, yloc
|
|
1438
|
+
|
|
1439
|
+
# If all positions have overlap, return the first candidate
|
|
1440
|
+
return candidates[0]
|
|
1441
|
+
|
|
1442
|
+
|
|
1443
|
+
def get_plot_elements_bounds(ax, slope_data):
|
|
1444
|
+
"""
|
|
1445
|
+
Get bounding boxes of existing plot elements to avoid overlaps.
|
|
1446
|
+
|
|
1447
|
+
Parameters:
|
|
1448
|
+
ax: matplotlib Axes object
|
|
1449
|
+
slope_data: Dictionary containing slope data
|
|
1450
|
+
|
|
1451
|
+
Returns:
|
|
1452
|
+
List of (x_min, x_max, y_min, y_max) tuples for plot elements
|
|
1453
|
+
"""
|
|
1454
|
+
bounds = []
|
|
1455
|
+
|
|
1456
|
+
# Get axis limits
|
|
1457
|
+
x_min, x_max = ax.get_xlim()
|
|
1458
|
+
y_min, y_max = ax.get_ylim()
|
|
1459
|
+
|
|
1460
|
+
# Profile lines bounds
|
|
1461
|
+
if 'profile_lines' in slope_data:
|
|
1462
|
+
for line in slope_data['profile_lines']:
|
|
1463
|
+
if line:
|
|
1464
|
+
xs = [p[0] for p in line]
|
|
1465
|
+
ys = [p[1] for p in line]
|
|
1466
|
+
bounds.append((min(xs), max(xs), min(ys), max(ys)))
|
|
1467
|
+
|
|
1468
|
+
# Distributed loads bounds
|
|
1469
|
+
if 'dloads' in slope_data and slope_data['dloads']:
|
|
1470
|
+
for dload_set in slope_data['dloads']:
|
|
1471
|
+
if dload_set:
|
|
1472
|
+
xs = [p['X'] for p in dload_set]
|
|
1473
|
+
ys = [p['Y'] for p in dload_set]
|
|
1474
|
+
bounds.append((min(xs), max(xs), min(ys), max(ys)))
|
|
1475
|
+
|
|
1476
|
+
# Reinforcement lines bounds
|
|
1477
|
+
if 'reinforce_lines' in slope_data and slope_data['reinforce_lines']:
|
|
1478
|
+
for line in slope_data['reinforce_lines']:
|
|
1479
|
+
if line:
|
|
1480
|
+
xs = [p['X'] for p in line]
|
|
1481
|
+
ys = [p['Y'] for p in line]
|
|
1482
|
+
bounds.append((min(xs), max(xs), min(ys), max(ys)))
|
|
1483
|
+
|
|
1484
|
+
return bounds
|