xslope 0.1.13__py3-none-any.whl → 0.1.15__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/_version.py +1 -1
- xslope/plot.py +223 -31
- xslope/search.py +22 -8
- xslope/slice.py +44 -35
- {xslope-0.1.13.dist-info → xslope-0.1.15.dist-info}/METADATA +1 -1
- {xslope-0.1.13.dist-info → xslope-0.1.15.dist-info}/RECORD +10 -10
- {xslope-0.1.13.dist-info → xslope-0.1.15.dist-info}/LICENSE +0 -0
- {xslope-0.1.13.dist-info → xslope-0.1.15.dist-info}/NOTICE +0 -0
- {xslope-0.1.13.dist-info → xslope-0.1.15.dist-info}/WHEEL +0 -0
- {xslope-0.1.13.dist-info → xslope-0.1.15.dist-info}/top_level.txt +0 -0
xslope/_version.py
CHANGED
xslope/plot.py
CHANGED
|
@@ -466,22 +466,159 @@ def plot_seepage_bc_lines(ax, slope_data):
|
|
|
466
466
|
label="Exit Face",
|
|
467
467
|
)
|
|
468
468
|
|
|
469
|
-
def plot_tcrack_surface(ax,
|
|
469
|
+
def plot_tcrack_surface(ax, slope_data):
|
|
470
470
|
"""
|
|
471
|
-
Plots the tension crack surface as a thin dashed red line.
|
|
471
|
+
Plots the tension crack surface as a thin dashed red line, clipped to max_depth.
|
|
472
472
|
|
|
473
473
|
Parameters:
|
|
474
474
|
ax: matplotlib Axes object
|
|
475
|
-
|
|
475
|
+
slope_data: Dictionary containing tcrack_surface and max_depth
|
|
476
476
|
|
|
477
477
|
Returns:
|
|
478
478
|
None
|
|
479
479
|
"""
|
|
480
|
+
tcrack_surface = slope_data.get('tcrack_surface')
|
|
480
481
|
if tcrack_surface is None:
|
|
481
482
|
return
|
|
482
483
|
|
|
483
|
-
|
|
484
|
-
|
|
484
|
+
color = 'red'
|
|
485
|
+
linestyle = ':'
|
|
486
|
+
linewidth = 1.5
|
|
487
|
+
|
|
488
|
+
max_depth = slope_data.get('max_depth')
|
|
489
|
+
if max_depth is None:
|
|
490
|
+
# No clipping needed
|
|
491
|
+
x_vals, y_vals = tcrack_surface.xy
|
|
492
|
+
ax.plot(x_vals, y_vals, linestyle=linestyle, color=color, linewidth=linewidth, label='Tension Crack Depth')
|
|
493
|
+
return
|
|
494
|
+
|
|
495
|
+
# Get coordinates and clip to max_depth with interpolation
|
|
496
|
+
coords = list(tcrack_surface.coords)
|
|
497
|
+
x_clipped = []
|
|
498
|
+
y_clipped = []
|
|
499
|
+
|
|
500
|
+
for i in range(len(coords)):
|
|
501
|
+
x1, y1 = coords[i]
|
|
502
|
+
|
|
503
|
+
if y1 >= max_depth:
|
|
504
|
+
# Point is above max_depth, include it
|
|
505
|
+
x_clipped.append(x1)
|
|
506
|
+
y_clipped.append(y1)
|
|
507
|
+
|
|
508
|
+
# Check if segment crosses max_depth (need to interpolate)
|
|
509
|
+
if i < len(coords) - 1:
|
|
510
|
+
x2, y2 = coords[i + 1]
|
|
511
|
+
# Check if segment crosses max_depth
|
|
512
|
+
if (y1 < max_depth and y2 >= max_depth) or (y1 >= max_depth and y2 < max_depth):
|
|
513
|
+
# Interpolate to find crossing point
|
|
514
|
+
t = (max_depth - y1) / (y2 - y1)
|
|
515
|
+
x_cross = x1 + t * (x2 - x1)
|
|
516
|
+
x_clipped.append(x_cross)
|
|
517
|
+
y_clipped.append(max_depth)
|
|
518
|
+
|
|
519
|
+
if x_clipped:
|
|
520
|
+
ax.plot(x_clipped, y_clipped, linestyle=linestyle, color=color, linewidth=linewidth, label='Tension Crack Depth')
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def plot_tcrack_water_force(ax, slice_df, slope_data):
|
|
524
|
+
"""
|
|
525
|
+
Plots the triangular water pressure distribution on the tension crack face.
|
|
526
|
+
|
|
527
|
+
The water in the tension crack creates a triangular pressure distribution
|
|
528
|
+
acting horizontally on the side of the top slice. Pressure is zero at the
|
|
529
|
+
water surface and maximum (gamma_w * water_depth) at the bottom.
|
|
530
|
+
The triangle is drawn on the outside of the slice, with arrows pointing
|
|
531
|
+
toward the slice to show force direction. The base of the triangle is
|
|
532
|
+
scaled to equal the water depth.
|
|
533
|
+
|
|
534
|
+
Parameters:
|
|
535
|
+
ax: matplotlib Axes object
|
|
536
|
+
slice_df: DataFrame containing slice data with 't' and 'y_t' columns
|
|
537
|
+
slope_data: Dictionary containing slope data including tcrack_water
|
|
538
|
+
|
|
539
|
+
Returns:
|
|
540
|
+
None
|
|
541
|
+
"""
|
|
542
|
+
tcrack_water = slope_data.get('tcrack_water', 0)
|
|
543
|
+
if tcrack_water <= 0:
|
|
544
|
+
return
|
|
545
|
+
|
|
546
|
+
# Find the slice with the tension crack force
|
|
547
|
+
t_forces = slice_df['t'].abs()
|
|
548
|
+
if t_forces.max() == 0:
|
|
549
|
+
return
|
|
550
|
+
|
|
551
|
+
# Get the slice with the tension crack force
|
|
552
|
+
tcrack_slice_idx = t_forces.idxmax()
|
|
553
|
+
tcrack_slice = slice_df.loc[tcrack_slice_idx]
|
|
554
|
+
|
|
555
|
+
t_force = tcrack_slice['t']
|
|
556
|
+
y_rb = tcrack_slice['y_rb']
|
|
557
|
+
y_rt = tcrack_slice['y_rt']
|
|
558
|
+
|
|
559
|
+
# Determine if right-facing or left-facing based on sign of t
|
|
560
|
+
# Negative t means right-facing (force acts to the right, on left side of first slice)
|
|
561
|
+
# Positive t means left-facing (force acts to the left, on right side of last slice)
|
|
562
|
+
right_facing = t_force < 0
|
|
563
|
+
|
|
564
|
+
if right_facing:
|
|
565
|
+
# Water on left side of slice, triangle extends left (outside), arrows point right (into slice)
|
|
566
|
+
x_base = tcrack_slice['x_l']
|
|
567
|
+
triangle_direction = -1 # triangle extends left (outside the slice)
|
|
568
|
+
arrow_direction = 1 # arrows point right (into the slice)
|
|
569
|
+
else:
|
|
570
|
+
# Water on right side of slice, triangle extends right (outside), arrows point left (into slice)
|
|
571
|
+
x_base = tcrack_slice['x_r']
|
|
572
|
+
triangle_direction = 1 # triangle extends right (outside the slice)
|
|
573
|
+
arrow_direction = -1 # arrows point left (into the slice)
|
|
574
|
+
|
|
575
|
+
# Water surface is at ground level, bottom of water is at y_rb
|
|
576
|
+
y_water_top = y_rt # top of water (at ground surface)
|
|
577
|
+
y_water_bottom = y_rb # bottom of water (at failure surface)
|
|
578
|
+
water_depth = y_water_top - y_water_bottom
|
|
579
|
+
|
|
580
|
+
if water_depth <= 0:
|
|
581
|
+
return
|
|
582
|
+
|
|
583
|
+
# Scale so that the base of the triangle equals the water depth
|
|
584
|
+
max_length = tcrack_water # base of triangle = water depth
|
|
585
|
+
|
|
586
|
+
# Arrow head dimensions (same style as distributed loads)
|
|
587
|
+
head_length = max_length / 8
|
|
588
|
+
head_width = head_length * 0.8
|
|
589
|
+
|
|
590
|
+
# Draw triangular pressure distribution (on outside of slice)
|
|
591
|
+
num_arrows = 5
|
|
592
|
+
y_positions = np.linspace(y_water_bottom, y_water_top, num_arrows + 1)[:-1]
|
|
593
|
+
|
|
594
|
+
for y_pos in y_positions:
|
|
595
|
+
# Arrow length proportional to depth (0 at top, max_length at bottom)
|
|
596
|
+
depth_from_surface = y_water_top - y_pos
|
|
597
|
+
arrow_length = max_length * (depth_from_surface / water_depth)
|
|
598
|
+
|
|
599
|
+
if arrow_length < 0.1:
|
|
600
|
+
continue # Skip very short arrows
|
|
601
|
+
|
|
602
|
+
# Arrow starts from outside (triangle edge) and points toward slice
|
|
603
|
+
x_start = x_base + arrow_length * triangle_direction
|
|
604
|
+
dx = -arrow_length * triangle_direction # direction toward slice
|
|
605
|
+
|
|
606
|
+
# Draw arrow using same style as distributed loads
|
|
607
|
+
if head_length > arrow_length:
|
|
608
|
+
# Draw a simple line without arrowhead for short arrows
|
|
609
|
+
ax.plot([x_start, x_base], [y_pos, y_pos],
|
|
610
|
+
color='blue', linewidth=2, alpha=0.7)
|
|
611
|
+
else:
|
|
612
|
+
ax.arrow(x_start, y_pos, dx, 0,
|
|
613
|
+
head_width=head_width, head_length=head_length,
|
|
614
|
+
fc='blue', ec='blue', alpha=0.7,
|
|
615
|
+
length_includes_head=True)
|
|
616
|
+
|
|
617
|
+
# Draw the triangular outline (pressure diagram) on outside of slice
|
|
618
|
+
triangle_x = [x_base, x_base + max_length * triangle_direction, x_base]
|
|
619
|
+
triangle_y = [y_water_top, y_water_bottom, y_water_bottom]
|
|
620
|
+
ax.fill(triangle_x, triangle_y, color='lightblue', alpha=0.3, edgecolor='blue', linewidth=1)
|
|
621
|
+
|
|
485
622
|
|
|
486
623
|
def plot_dloads(ax, slope_data):
|
|
487
624
|
"""
|
|
@@ -635,7 +772,9 @@ def plot_circles(ax, slope_data):
|
|
|
635
772
|
None
|
|
636
773
|
"""
|
|
637
774
|
circles = slope_data['circles']
|
|
638
|
-
|
|
775
|
+
tcrack_depth = slope_data.get('tcrack_depth', 0)
|
|
776
|
+
|
|
777
|
+
for i, circle in enumerate(circles):
|
|
639
778
|
Xo = circle['Xo']
|
|
640
779
|
Yo = circle['Yo']
|
|
641
780
|
R = circle['R']
|
|
@@ -644,11 +783,12 @@ def plot_circles(ax, slope_data):
|
|
|
644
783
|
# y_circle = Yo + R * np.sin(theta)
|
|
645
784
|
# ax.plot(x_circle, y_circle, 'r--', label='Circle')
|
|
646
785
|
|
|
647
|
-
# Plot the portion of the circle in the slope
|
|
786
|
+
# Plot the portion of the circle in the slope (clipped to tension crack if present)
|
|
648
787
|
ground_surface = slope_data['ground_surface']
|
|
649
|
-
success, result = generate_failure_surface(ground_surface, circular=True, circle=circle)
|
|
788
|
+
success, result = generate_failure_surface(ground_surface, circular=True, circle=circle, tcrack_depth=tcrack_depth)
|
|
650
789
|
if not success:
|
|
651
|
-
|
|
790
|
+
print(f"Warning: Circle {i+1} (Xo={Xo:.2f}, Yo={Yo:.2f}, R={R:.2f}) could not be plotted: {result}")
|
|
791
|
+
continue
|
|
652
792
|
# result = (x_min, x_max, y_left, y_right, clipped_surface)
|
|
653
793
|
x_min, x_max, y_left, y_right, clipped_surface = result
|
|
654
794
|
if not isinstance(clipped_surface, LineString):
|
|
@@ -673,11 +813,16 @@ def plot_circles(ax, slope_data):
|
|
|
673
813
|
dx /= length
|
|
674
814
|
dy /= length
|
|
675
815
|
|
|
676
|
-
#
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
816
|
+
# Draw arrow with pixel-based head size
|
|
817
|
+
ax.annotate('',
|
|
818
|
+
xy=(Xo + dx * R, Yo + dy * R), # arrow tip
|
|
819
|
+
xytext=(Xo, Yo), # arrow start
|
|
820
|
+
arrowprops=dict(
|
|
821
|
+
arrowstyle='-|>',
|
|
822
|
+
color='red',
|
|
823
|
+
lw=1.0, # shaft width in points
|
|
824
|
+
mutation_scale=20 # head size in points
|
|
825
|
+
))
|
|
681
826
|
|
|
682
827
|
def plot_non_circ(ax, non_circ):
|
|
683
828
|
"""
|
|
@@ -692,7 +837,12 @@ def plot_non_circ(ax, non_circ):
|
|
|
692
837
|
"""
|
|
693
838
|
if not non_circ or len(non_circ) == 0:
|
|
694
839
|
return
|
|
695
|
-
|
|
840
|
+
# Handle both dict format {'X': x, 'Y': y} and tuple format (x, y)
|
|
841
|
+
if isinstance(non_circ[0], dict):
|
|
842
|
+
xs = [p['X'] for p in non_circ]
|
|
843
|
+
ys = [p['Y'] for p in non_circ]
|
|
844
|
+
else:
|
|
845
|
+
xs, ys = zip(*non_circ)
|
|
696
846
|
ax.plot(xs, ys, 'r--', label='Non-Circular Surface')
|
|
697
847
|
|
|
698
848
|
def plot_lem_material_table(ax, materials, xloc=0.6, yloc=0.7):
|
|
@@ -1183,7 +1333,18 @@ def compute_ylim(data, slice_df, scale_frac=0.5, pad_fraction=0.1):
|
|
|
1183
1333
|
y_min -= max_bar
|
|
1184
1334
|
y_max += max_bar
|
|
1185
1335
|
|
|
1186
|
-
# 4)
|
|
1336
|
+
# 4) account for distributed loads extending above ground surface
|
|
1337
|
+
gamma_w = data.get('gamma_water', 62.4)
|
|
1338
|
+
for dloads in [data.get('dloads', []), data.get('dloads2', [])]:
|
|
1339
|
+
if dloads:
|
|
1340
|
+
for line in dloads:
|
|
1341
|
+
for pt in line:
|
|
1342
|
+
# dload arrows extend above surface by load/gamma_w (water depth equivalent)
|
|
1343
|
+
load = pt.get('Normal', 0)
|
|
1344
|
+
if load > 0:
|
|
1345
|
+
y_max = max(y_max, pt.get('Y', 0) + load / gamma_w)
|
|
1346
|
+
|
|
1347
|
+
# 5) add a final small pad
|
|
1187
1348
|
pad = (y_max - y_min) * pad_fraction
|
|
1188
1349
|
return y_min - pad, y_max + pad
|
|
1189
1350
|
|
|
@@ -1231,11 +1392,11 @@ def plot_inputs(
|
|
|
1231
1392
|
slope_data,
|
|
1232
1393
|
title="Slope Geometry and Inputs",
|
|
1233
1394
|
figsize=(12, 6),
|
|
1234
|
-
mat_table=
|
|
1395
|
+
mat_table=False,
|
|
1235
1396
|
save_png=False,
|
|
1236
1397
|
dpi=300,
|
|
1237
1398
|
mode="lem",
|
|
1238
|
-
tab_loc="
|
|
1399
|
+
tab_loc="top",
|
|
1239
1400
|
legend_ncol="auto",
|
|
1240
1401
|
legend_max_cols=6,
|
|
1241
1402
|
legend_max_rows=4,
|
|
@@ -1287,7 +1448,7 @@ def plot_inputs(
|
|
|
1287
1448
|
if mode == "seep":
|
|
1288
1449
|
plot_seepage_bc_lines(ax, slope_data)
|
|
1289
1450
|
plot_dloads(ax, slope_data)
|
|
1290
|
-
plot_tcrack_surface(ax, slope_data
|
|
1451
|
+
plot_tcrack_surface(ax, slope_data)
|
|
1291
1452
|
plot_reinforcement_lines(ax, slope_data)
|
|
1292
1453
|
|
|
1293
1454
|
if slope_data['circular']:
|
|
@@ -1487,7 +1648,8 @@ def plot_solution(slope_data, slice_df, failure_surface, results, figsize=(12, 7
|
|
|
1487
1648
|
plot_failure_surface(ax, failure_surface)
|
|
1488
1649
|
plot_piezo_line(ax, slope_data)
|
|
1489
1650
|
plot_dloads(ax, slope_data)
|
|
1490
|
-
plot_tcrack_surface(ax, slope_data
|
|
1651
|
+
plot_tcrack_surface(ax, slope_data)
|
|
1652
|
+
plot_tcrack_water_force(ax, slice_df, slope_data)
|
|
1491
1653
|
plot_reinforcement_lines(ax, slope_data)
|
|
1492
1654
|
if slice_numbers:
|
|
1493
1655
|
plot_slice_numbers(ax, slice_df)
|
|
@@ -1617,7 +1779,7 @@ def plot_search_path(ax, search_path):
|
|
|
1617
1779
|
ax.arrow(start['x'], start['y'], dx, dy,
|
|
1618
1780
|
head_width=1, head_length=2, fc='green', ec='green', length_includes_head=True)
|
|
1619
1781
|
|
|
1620
|
-
def plot_circular_search_results(slope_data, fs_cache, search_path=None, highlight_fs=True, figsize=(12, 7), save_png=False, dpi=300):
|
|
1782
|
+
def plot_circular_search_results(slope_data, fs_cache, search_path=None, circle_cache=None, highlight_fs=True, figsize=(12, 7), save_png=False, dpi=300):
|
|
1621
1783
|
"""
|
|
1622
1784
|
Creates a plot showing the results of a circular failure surface search.
|
|
1623
1785
|
|
|
@@ -1625,6 +1787,7 @@ def plot_circular_search_results(slope_data, fs_cache, search_path=None, highlig
|
|
|
1625
1787
|
slope_data: Dictionary containing plot data
|
|
1626
1788
|
fs_cache: List of dictionaries containing failure surface data and FS values
|
|
1627
1789
|
search_path: List of dictionaries containing search path coordinates
|
|
1790
|
+
circle_cache: List of dictionaries containing all tested circles (for plotting)
|
|
1628
1791
|
highlight_fs: Boolean indicating whether to highlight the critical failure surface
|
|
1629
1792
|
figsize: Tuple of (width, height) in inches for the plot
|
|
1630
1793
|
|
|
@@ -1637,10 +1800,33 @@ def plot_circular_search_results(slope_data, fs_cache, search_path=None, highlig
|
|
|
1637
1800
|
plot_max_depth(ax, slope_data['profile_lines'], slope_data['max_depth'])
|
|
1638
1801
|
plot_piezo_line(ax, slope_data)
|
|
1639
1802
|
plot_dloads(ax, slope_data)
|
|
1640
|
-
plot_tcrack_surface(ax, slope_data
|
|
1641
|
-
|
|
1642
|
-
|
|
1803
|
+
plot_tcrack_surface(ax, slope_data)
|
|
1804
|
+
|
|
1805
|
+
# Plot all tested circles from circle_cache (light gray)
|
|
1806
|
+
if circle_cache:
|
|
1807
|
+
first_plotted = True
|
|
1808
|
+
for result in circle_cache:
|
|
1809
|
+
surface = result.get('failure_surface')
|
|
1810
|
+
if surface is None or surface.is_empty:
|
|
1811
|
+
continue
|
|
1812
|
+
x, y = zip(*surface.coords)
|
|
1813
|
+
label = 'Tested Circle' if first_plotted else None
|
|
1814
|
+
ax.plot(x, y, color='gray', linestyle='-', linewidth=0.5, alpha=0.5, label=label)
|
|
1815
|
+
first_plotted = False
|
|
1816
|
+
|
|
1817
|
+
# Plot only the critical circle from fs_cache (red)
|
|
1818
|
+
if fs_cache:
|
|
1819
|
+
critical = fs_cache[0]
|
|
1820
|
+
surface = critical.get('failure_surface')
|
|
1821
|
+
if surface is not None and not surface.is_empty:
|
|
1822
|
+
x, y = zip(*surface.coords)
|
|
1823
|
+
ax.plot(x, y, color='red', linestyle='-', linewidth=2, label='Critical Circle')
|
|
1824
|
+
# Plot critical circle center
|
|
1825
|
+
ax.plot(critical['Xo'], critical['Yo'], 'ro', markersize=5)
|
|
1826
|
+
|
|
1827
|
+
# Plot all circle centers from fs_cache
|
|
1643
1828
|
plot_circle_centers(ax, fs_cache)
|
|
1829
|
+
|
|
1644
1830
|
if search_path:
|
|
1645
1831
|
plot_search_path(ax, search_path)
|
|
1646
1832
|
|
|
@@ -1683,17 +1869,23 @@ def plot_noncircular_search_results(slope_data, fs_cache, search_path=None, high
|
|
|
1683
1869
|
plot_max_depth(ax, slope_data['profile_lines'], slope_data['max_depth'])
|
|
1684
1870
|
plot_piezo_line(ax, slope_data)
|
|
1685
1871
|
plot_dloads(ax, slope_data)
|
|
1686
|
-
plot_tcrack_surface(ax, slope_data
|
|
1872
|
+
plot_tcrack_surface(ax, slope_data)
|
|
1687
1873
|
|
|
1688
1874
|
# Plot all failure surfaces from cache
|
|
1875
|
+
first_tested = True
|
|
1689
1876
|
for i, result in reversed(list(enumerate(fs_cache))):
|
|
1690
1877
|
surface = result['failure_surface']
|
|
1691
1878
|
if surface is None or surface.is_empty:
|
|
1692
1879
|
continue
|
|
1693
1880
|
x, y = zip(*surface.coords)
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1881
|
+
if i == 0:
|
|
1882
|
+
# Critical surface
|
|
1883
|
+
ax.plot(x, y, color='red', linestyle='-', linewidth=2, alpha=1.0, label='Critical Surface')
|
|
1884
|
+
else:
|
|
1885
|
+
# Tested surfaces
|
|
1886
|
+
label = 'Tested Surface' if first_tested else None
|
|
1887
|
+
ax.plot(x, y, color='gray', linestyle='-', linewidth=1, alpha=0.6, label=label)
|
|
1888
|
+
first_tested = False
|
|
1697
1889
|
|
|
1698
1890
|
# Plot search path if provided
|
|
1699
1891
|
if search_path:
|
|
@@ -1717,14 +1909,14 @@ def plot_noncircular_search_results(slope_data, fs_cache, search_path=None, high
|
|
|
1717
1909
|
ax.set_xlabel("x")
|
|
1718
1910
|
ax.set_ylabel("y")
|
|
1719
1911
|
ax.grid(False)
|
|
1720
|
-
ax.legend()
|
|
1912
|
+
ax.legend(loc='upper center', bbox_to_anchor=(0.5, -0.15), ncol=4)
|
|
1721
1913
|
|
|
1722
1914
|
if highlight_fs and fs_cache:
|
|
1723
1915
|
critical_fs = fs_cache[0]['FS']
|
|
1724
1916
|
ax.set_title(f"Critical Factor of Safety = {critical_fs:.3f}")
|
|
1725
1917
|
|
|
1726
1918
|
plt.tight_layout()
|
|
1727
|
-
|
|
1919
|
+
|
|
1728
1920
|
if save_png:
|
|
1729
1921
|
filename = 'plot_noncircular_search_results.png'
|
|
1730
1922
|
plt.savefig(filename, dpi=dpi, bbox_inches='tight')
|
|
@@ -1750,7 +1942,7 @@ def plot_reliability_results(slope_data, reliability_data, figsize=(12, 7), save
|
|
|
1750
1942
|
plot_max_depth(ax, slope_data['profile_lines'], slope_data['max_depth'])
|
|
1751
1943
|
plot_piezo_line(ax, slope_data)
|
|
1752
1944
|
plot_dloads(ax, slope_data)
|
|
1753
|
-
plot_tcrack_surface(ax, slope_data
|
|
1945
|
+
plot_tcrack_surface(ax, slope_data)
|
|
1754
1946
|
|
|
1755
1947
|
# Plot reliability-specific failure surfaces
|
|
1756
1948
|
fs_cache = reliability_data['fs_cache']
|
xslope/search.py
CHANGED
|
@@ -30,9 +30,11 @@ def circular_search(slope_data, method_name, rapid=False, tol=1e-2, max_iter=50,
|
|
|
30
30
|
list of dict: sorted fs_cache by FS
|
|
31
31
|
bool: convergence flag
|
|
32
32
|
list of dict: search path
|
|
33
|
+
list of dict: circle_cache - all circles tested during search
|
|
33
34
|
"""
|
|
34
35
|
|
|
35
36
|
solver = getattr(solve, method_name)
|
|
37
|
+
circle_cache = [] # Store ALL circles tested for plotting
|
|
36
38
|
|
|
37
39
|
start_time = time.time() # Start timing
|
|
38
40
|
|
|
@@ -46,7 +48,7 @@ def circular_search(slope_data, method_name, rapid=False, tol=1e-2, max_iter=50,
|
|
|
46
48
|
circles = slope_data['circles']
|
|
47
49
|
max_depth = slope_data['max_depth']
|
|
48
50
|
|
|
49
|
-
def optimize_depth(x, y, depth_guess, depth_step_init, depth_shrink_factor, tol_frac, fs_fail, diagnostic=False):
|
|
51
|
+
def optimize_depth(x, y, depth_guess, depth_step_init, depth_shrink_factor, tol_frac, fs_fail, circle_cache, diagnostic=False):
|
|
50
52
|
depth_step = min(10.0, depth_step_init)
|
|
51
53
|
best_depth = max(depth_guess, max_depth)
|
|
52
54
|
best_fs = fs_fail
|
|
@@ -78,6 +80,17 @@ def circular_search(slope_data, method_name, rapid=False, tol=1e-2, max_iter=50,
|
|
|
78
80
|
FS = solver_result['FS'] if solver_success else fs_fail
|
|
79
81
|
fs_results.append((FS, d, df_slices, failure_surface, solver_result))
|
|
80
82
|
|
|
83
|
+
# Add to circle_cache for plotting all tested circles
|
|
84
|
+
if FS != fs_fail:
|
|
85
|
+
circle_cache.append({
|
|
86
|
+
"Xo": x,
|
|
87
|
+
"Yo": y,
|
|
88
|
+
"Depth": d,
|
|
89
|
+
"R": y - d,
|
|
90
|
+
"FS": FS,
|
|
91
|
+
"failure_surface": failure_surface
|
|
92
|
+
})
|
|
93
|
+
|
|
81
94
|
fs_results.sort(key=lambda t: t[0])
|
|
82
95
|
best_fs, best_depth, best_df, best_surface, best_solver_result = fs_results[0]
|
|
83
96
|
|
|
@@ -98,7 +111,7 @@ def circular_search(slope_data, method_name, rapid=False, tol=1e-2, max_iter=50,
|
|
|
98
111
|
|
|
99
112
|
return best_depth, best_fs, best_df, best_surface, best_solver_result
|
|
100
113
|
|
|
101
|
-
def evaluate_grid(x0, y0, grid_size, depth_guess, slope_data, diagnostic=False, fs_cache=None):
|
|
114
|
+
def evaluate_grid(x0, y0, grid_size, depth_guess, slope_data, diagnostic=False, fs_cache=None, circle_cache=None):
|
|
102
115
|
if fs_cache is None:
|
|
103
116
|
fs_cache = {}
|
|
104
117
|
|
|
@@ -116,7 +129,7 @@ def circular_search(slope_data, method_name, rapid=False, tol=1e-2, max_iter=50,
|
|
|
116
129
|
depth_step_init = grid_size * 0.75
|
|
117
130
|
d, FS, df_slices, failure_surface, solver_result = optimize_depth(
|
|
118
131
|
x, y, depth_guess, depth_step_init, depth_shrink_factor=0.25, tol_frac=0.01, fs_fail=fs_fail,
|
|
119
|
-
diagnostic=diagnostic
|
|
132
|
+
circle_cache=circle_cache, diagnostic=diagnostic
|
|
120
133
|
)
|
|
121
134
|
|
|
122
135
|
fs_cache[(x, y)] = {
|
|
@@ -143,6 +156,7 @@ def circular_search(slope_data, method_name, rapid=False, tol=1e-2, max_iter=50,
|
|
|
143
156
|
|
|
144
157
|
# === Step 1: Evaluate starting circles ===
|
|
145
158
|
all_starts = []
|
|
159
|
+
fs_cache = {} # Shared cache for all starting circles
|
|
146
160
|
for i, start_circle in enumerate(circles):
|
|
147
161
|
x0 = start_circle['Xo']
|
|
148
162
|
y0 = start_circle['Yo']
|
|
@@ -151,11 +165,11 @@ def circular_search(slope_data, method_name, rapid=False, tol=1e-2, max_iter=50,
|
|
|
151
165
|
print(f"\n[⏱ starting circle {i+1}] x={x0:.2f}, y={y0:.2f}, r={r0:.2f}")
|
|
152
166
|
grid_size = r0 * 0.15
|
|
153
167
|
depth_guess = start_circle['Depth']
|
|
154
|
-
fs_cache, best_point = evaluate_grid(x0, y0, grid_size, depth_guess, slope_data, diagnostic=diagnostic)
|
|
155
|
-
all_starts.append((start_circle, best_point
|
|
168
|
+
fs_cache, best_point = evaluate_grid(x0, y0, grid_size, depth_guess, slope_data, diagnostic=diagnostic, fs_cache=fs_cache, circle_cache=circle_cache)
|
|
169
|
+
all_starts.append((start_circle, best_point))
|
|
156
170
|
|
|
157
171
|
all_starts.sort(key=lambda t: t[1]['FS'])
|
|
158
|
-
start_circle, best_start
|
|
172
|
+
start_circle, best_start = all_starts[0]
|
|
159
173
|
x0 = best_start['Xo']
|
|
160
174
|
y0 = best_start['Yo']
|
|
161
175
|
depth_guess = best_start['Depth']
|
|
@@ -174,7 +188,7 @@ def circular_search(slope_data, method_name, rapid=False, tol=1e-2, max_iter=50,
|
|
|
174
188
|
|
|
175
189
|
for iteration in range(max_iter):
|
|
176
190
|
print(f"[🔁 iteration {iteration+1}] center=({x0:.2f}, {y0:.2f}), FS={best_fs:.4f}, grid={grid_size:.4f}")
|
|
177
|
-
fs_cache, best_point = evaluate_grid(x0, y0, grid_size, depth_guess, slope_data, diagnostic=diagnostic, fs_cache=fs_cache)
|
|
191
|
+
fs_cache, best_point = evaluate_grid(x0, y0, grid_size, depth_guess, slope_data, diagnostic=diagnostic, fs_cache=fs_cache, circle_cache=circle_cache)
|
|
178
192
|
|
|
179
193
|
if best_point['FS'] < best_fs:
|
|
180
194
|
best_fs = best_point['FS']
|
|
@@ -196,7 +210,7 @@ def circular_search(slope_data, method_name, rapid=False, tol=1e-2, max_iter=50,
|
|
|
196
210
|
print(f"\n[❌ max iterations reached] FS={best_fs:.4f} at (x={x0:.2f}, y={y0:.2f})")
|
|
197
211
|
|
|
198
212
|
sorted_fs_cache = sorted(fs_cache.values(), key=lambda d: d['FS'])
|
|
199
|
-
return sorted_fs_cache, converged, search_path
|
|
213
|
+
return sorted_fs_cache, converged, search_path, circle_cache
|
|
200
214
|
|
|
201
215
|
def noncircular_search(slope_data, method_name, rapid=False, diagnostic=True, movement_distance=4.0, shrink_factor=0.8, fs_tol=0.001, max_iter=100, move_tol=0.1):
|
|
202
216
|
"""
|
xslope/slice.py
CHANGED
|
@@ -264,22 +264,6 @@ def get_sorted_intersections(failure_surface, ground_surface, circle_params=None
|
|
|
264
264
|
return True, "", pruned
|
|
265
265
|
|
|
266
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
267
|
def generate_failure_surface(ground_surface, circular, circle=None, non_circ=None, tcrack_depth=0):
|
|
284
268
|
"""
|
|
285
269
|
Generates a failure surface based on either a circular or non-circular definition.
|
|
@@ -311,7 +295,7 @@ def generate_failure_surface(ground_surface, circular, circle=None, non_circ=Non
|
|
|
311
295
|
else:
|
|
312
296
|
return False, "Either a circular or non-circular failure surface must be provided."
|
|
313
297
|
|
|
314
|
-
# --- Step 2: Intersect with original ground surface to determine slope facing ---
|
|
298
|
+
# --- Step 2: Intersect with original ground surface to determine slope facing and toe ---
|
|
315
299
|
if circular and circle:
|
|
316
300
|
success, msg, points = get_sorted_intersections(failure_surface, ground_surface, circle_params=circle)
|
|
317
301
|
else:
|
|
@@ -322,41 +306,64 @@ def generate_failure_surface(ground_surface, circular, circle=None, non_circ=Non
|
|
|
322
306
|
x_min, x_max = points[0].x, points[1].x
|
|
323
307
|
y_left, y_right = points[0].y, points[1].y
|
|
324
308
|
right_facing = y_left > y_right
|
|
325
|
-
x_center = 0.5 * (x_min + x_max)
|
|
326
309
|
|
|
327
|
-
# --- Step 3: If tension crack exists,
|
|
310
|
+
# --- Step 3: If tension crack exists, find intersection with tension crack surface ---
|
|
328
311
|
if tcrack_depth > 0:
|
|
329
|
-
|
|
312
|
+
# Create tension crack surface as parallel offset of entire ground surface
|
|
313
|
+
tcrack_surface = LineString([(x, y - tcrack_depth) for x, y in ground_surface.coords])
|
|
314
|
+
|
|
315
|
+
# Find intersection of failure surface with tension crack surface
|
|
330
316
|
if circular and circle:
|
|
331
|
-
|
|
317
|
+
tcrack_points = circle_polyline_intersections(Xo, Yo, R, tcrack_surface)
|
|
332
318
|
else:
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
319
|
+
tcrack_intersection = failure_surface.intersection(tcrack_surface)
|
|
320
|
+
if isinstance(tcrack_intersection, Point):
|
|
321
|
+
tcrack_points = [tcrack_intersection]
|
|
322
|
+
elif isinstance(tcrack_intersection, MultiPoint):
|
|
323
|
+
tcrack_points = list(tcrack_intersection.geoms)
|
|
324
|
+
elif isinstance(tcrack_intersection, GeometryCollection):
|
|
325
|
+
tcrack_points = [g for g in tcrack_intersection.geoms if isinstance(g, Point)]
|
|
326
|
+
else:
|
|
327
|
+
tcrack_points = []
|
|
328
|
+
|
|
329
|
+
if len(tcrack_points) >= 1:
|
|
330
|
+
# Sort tension crack intersection points by x
|
|
331
|
+
tcrack_points = sorted(tcrack_points, key=lambda p: p.x)
|
|
332
|
+
|
|
333
|
+
if right_facing:
|
|
334
|
+
# Right-facing slope: tension crack is on the left (upslope)
|
|
335
|
+
# Use leftmost tcrack intersection as new x_min
|
|
336
|
+
new_left = tcrack_points[0]
|
|
337
|
+
x_min = new_left.x
|
|
338
|
+
y_left = new_left.y
|
|
339
|
+
else:
|
|
340
|
+
# Left-facing slope: tension crack is on the right (upslope)
|
|
341
|
+
# Use rightmost tcrack intersection as new x_max
|
|
342
|
+
new_right = tcrack_points[-1]
|
|
343
|
+
x_max = new_right.x
|
|
344
|
+
y_right = new_right.y
|
|
338
345
|
|
|
339
346
|
# --- Step 4: Clip the failure surface between intersection x-range ---
|
|
340
347
|
# Filter coordinates within the x-range
|
|
341
348
|
filtered_coords = [pt for pt in failure_coords if x_min <= pt[0] <= x_max]
|
|
342
|
-
|
|
349
|
+
|
|
343
350
|
# Add the exact intersection points if they're not already in the filtered list
|
|
344
351
|
left_intersection = (x_min, y_left)
|
|
345
352
|
right_intersection = (x_max, y_right)
|
|
346
|
-
|
|
353
|
+
|
|
347
354
|
# Check if intersection points are already in the filtered list (with tolerance)
|
|
348
355
|
tol = 1e-6
|
|
349
356
|
has_left = any(abs(pt[0] - x_min) < tol and abs(pt[1] - y_left) < tol for pt in filtered_coords)
|
|
350
357
|
has_right = any(abs(pt[0] - x_max) < tol and abs(pt[1] - y_right) < tol for pt in filtered_coords)
|
|
351
|
-
|
|
358
|
+
|
|
352
359
|
if not has_left:
|
|
353
360
|
filtered_coords.insert(0, left_intersection)
|
|
354
361
|
if not has_right:
|
|
355
362
|
filtered_coords.append(right_intersection)
|
|
356
|
-
|
|
363
|
+
|
|
357
364
|
# Sort by x-coordinate to ensure proper ordering
|
|
358
365
|
filtered_coords.sort(key=lambda pt: pt[0])
|
|
359
|
-
|
|
366
|
+
|
|
360
367
|
clipped_surface = LineString(filtered_coords)
|
|
361
368
|
|
|
362
369
|
return True, (x_min, x_max, y_left, y_right, clipped_surface)
|
|
@@ -600,10 +607,11 @@ def generate_slices(slope_data, circle=None, non_circ=None, num_slices=40, debug
|
|
|
600
607
|
intersection = line_geom.intersection(circle_line)
|
|
601
608
|
if not intersection.is_empty:
|
|
602
609
|
if hasattr(intersection, 'x'):
|
|
603
|
-
|
|
610
|
+
if x_min <= intersection.x <= x_max:
|
|
611
|
+
fixed_xs.add(intersection.x)
|
|
604
612
|
elif hasattr(intersection, 'geoms'):
|
|
605
613
|
for geom in intersection.geoms:
|
|
606
|
-
if hasattr(geom, 'x'):
|
|
614
|
+
if hasattr(geom, 'x') and x_min <= geom.x <= x_max:
|
|
607
615
|
fixed_xs.add(geom.x)
|
|
608
616
|
else:
|
|
609
617
|
# For non-circular failure surfaces, use the original approach
|
|
@@ -611,10 +619,11 @@ def generate_slices(slope_data, circle=None, non_circ=None, num_slices=40, debug
|
|
|
611
619
|
intersection = LineString(profile_lines[i]['coords']).intersection(clipped_surface)
|
|
612
620
|
if not intersection.is_empty:
|
|
613
621
|
if hasattr(intersection, 'x'):
|
|
614
|
-
|
|
622
|
+
if x_min <= intersection.x <= x_max:
|
|
623
|
+
fixed_xs.add(intersection.x)
|
|
615
624
|
elif hasattr(intersection, 'geoms'):
|
|
616
625
|
for geom in intersection.geoms:
|
|
617
|
-
if hasattr(geom, 'x'):
|
|
626
|
+
if hasattr(geom, 'x') and x_min <= geom.x <= x_max:
|
|
618
627
|
fixed_xs.add(geom.x)
|
|
619
628
|
|
|
620
629
|
# Find intersections with piezometric lines
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
xslope/__init__.py,sha256=ZygAIkX6Nbjag1czWdQa-yP-GM1mBE_9ss21Xh__JFc,34
|
|
2
|
-
xslope/_version.py,sha256=
|
|
2
|
+
xslope/_version.py,sha256=hD1GXFmGgqIG0Tc3NOuueq-3OtmJ--iS4ruDqHiw4gM,51
|
|
3
3
|
xslope/advanced.py,sha256=-eL4-I36j6DfsheaGbOtclUHTFkkSn1k5k4YF9tOCvU,18278
|
|
4
4
|
xslope/fem.py,sha256=x3BWHqxqJd-K78NzLquqr8eLXWVdFMzyA2cQXTNqLTk,115719
|
|
5
5
|
xslope/fileio.py,sha256=BajiNljdwMMF2u2SqLpObZ-rimhgm3XyaFo14N6vZJI,38180
|
|
6
6
|
xslope/global_config.py,sha256=Cj8mbPidIuj5Ty-5cZM-c8H12kNvyHsk5_ofNGez-3M,2253
|
|
7
7
|
xslope/mesh copy.py,sha256=qtMH1yKFgHM4kNuIrxco7tKV86R3Dbf7Nok5j6MtlLY,131013
|
|
8
8
|
xslope/mesh.py,sha256=Kn2Um1wz50EA4xd5xpxgxxegxWOlpL3Brks0_8JLWFQ,139382
|
|
9
|
-
xslope/plot.py,sha256=
|
|
9
|
+
xslope/plot.py,sha256=P5M3kf4CFp6w2G4hugYdtA2bvyvbxd7gdVxJoSknyIU,94297
|
|
10
10
|
xslope/plot_fem.py,sha256=z2FLPbIx6yNIKYcgC-LcZz2jfx0WLYFL3xJgNvQ1t-c,69488
|
|
11
11
|
xslope/plot_seep.py,sha256=0s8R76c_34sUwA-L-_71Kt5BZSaIFbs-xV56M69XRU4,32960
|
|
12
|
-
xslope/search.py,sha256=
|
|
12
|
+
xslope/search.py,sha256=ZY0Gw14l_V-ORg7HKNHVAGud-peVg4ASdNfv2h0-zoE,17587
|
|
13
13
|
xslope/seep.py,sha256=AlZvp1kSPBfbNkpSZUNXqaefIRuT6BZPxhadmb5DFsk,93270
|
|
14
|
-
xslope/slice.py,sha256=
|
|
14
|
+
xslope/slice.py,sha256=zNbLtaMXdUMVGOgXLFJO-YvVWlsqPyHnDNHpAxzD0dQ,47100
|
|
15
15
|
xslope/solve.py,sha256=_whlUuSsDqqe0wtgLowucc9dXmC8Q6J9zbViTQOmo-I,49337
|
|
16
|
-
xslope-0.1.
|
|
17
|
-
xslope-0.1.
|
|
18
|
-
xslope-0.1.
|
|
19
|
-
xslope-0.1.
|
|
20
|
-
xslope-0.1.
|
|
21
|
-
xslope-0.1.
|
|
16
|
+
xslope-0.1.15.dist-info/LICENSE,sha256=NU5J88FUai_4Ixu5DqOQukA-gcXAyX8pXLhIg8OB3AM,10969
|
|
17
|
+
xslope-0.1.15.dist-info/METADATA,sha256=HM1g3oyEASm0o1LnHPfbUWbgHP89ej6NwLDd_pEOMG4,2028
|
|
18
|
+
xslope-0.1.15.dist-info/NOTICE,sha256=E-sbN0MWwvJC27Z-2_G4VUHIx4IsfvLDTmOstvY4-OQ,530
|
|
19
|
+
xslope-0.1.15.dist-info/WHEEL,sha256=beeZ86-EfXScwlR_HKu4SllMC9wUEj_8Z_4FJ3egI2w,91
|
|
20
|
+
xslope-0.1.15.dist-info/top_level.txt,sha256=5qHbWJ1R2pdTNIainFyrVtFk8R1tRcwIn0kzTuwuV1Q,7
|
|
21
|
+
xslope-0.1.15.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|