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 CHANGED
@@ -1,4 +1,4 @@
1
1
  __all__ = ["__version__"]
2
- __version__ = "0.1.13"
2
+ __version__ = "0.1.15"
3
3
 
4
4
 
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, tcrack_surface):
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
- tcrack_surface: Shapely LineString
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
- x_vals, y_vals = tcrack_surface.xy
484
- ax.plot(x_vals, y_vals, linestyle='--', color='red', linewidth=1.0, label='Tension Crack Depth')
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
- for circle in circles:
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
- continue # or handle error
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
- # Shorten shaft length slightly
677
- shaft_length = R - 5
678
-
679
- ax.arrow(Xo, Yo, dx * shaft_length, dy * shaft_length,
680
- head_width=5, head_length=5, fc='red', ec='red')
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
- xs, ys = zip(*non_circ)
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) add a final small pad
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=True,
1395
+ mat_table=False,
1235
1396
  save_png=False,
1236
1397
  dpi=300,
1237
1398
  mode="lem",
1238
- tab_loc="upper left",
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['tcrack_surface'])
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['tcrack_surface'])
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['tcrack_surface'])
1641
-
1642
- plot_failure_surfaces(ax, fs_cache)
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['tcrack_surface'])
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
- color = 'red' if i == 0 else 'gray'
1695
- lw = 2 if i == 0 else 1
1696
- ax.plot(x, y, color=color, linestyle='-', linewidth=lw, alpha=1.0 if i == 0 else 0.6)
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['tcrack_surface'])
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, fs_cache))
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, fs_cache = all_starts[0]
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, adjust surface and re-intersect ---
310
+ # --- Step 3: If tension crack exists, find intersection with tension crack surface ---
328
311
  if tcrack_depth > 0:
329
- modified_surface = adjust_ground_for_tcrack(ground_surface, x_center, tcrack_depth, right_facing)
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
- success, msg, points = get_sorted_intersections(failure_surface, modified_surface, circle_params=circle)
317
+ tcrack_points = circle_polyline_intersections(Xo, Yo, R, tcrack_surface)
332
318
  else:
333
- success, msg, points = get_sorted_intersections(failure_surface, modified_surface)
334
- if not success:
335
- return False, msg
336
- x_min, x_max = points[0].x, points[1].x
337
- y_left, y_right = points[0].y, points[1].y
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
- fixed_xs.add(intersection.x)
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
- fixed_xs.add(intersection.x)
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,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: xslope
3
- Version: 0.1.13
3
+ Version: 0.1.15
4
4
  Summary: Slope stability analysis (limit equilibrium and FEM) in Python.
5
5
  Author: Norman L. Jones
6
6
  Project-URL: Homepage, https://github.com/njones61/xslope
@@ -1,21 +1,21 @@
1
1
  xslope/__init__.py,sha256=ZygAIkX6Nbjag1czWdQa-yP-GM1mBE_9ss21Xh__JFc,34
2
- xslope/_version.py,sha256=XLyb183vl7CU3KttGJB7AeLEWBwd9veAWre8ysdY9ww,51
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=VkItm3kASiNL3hha-W6PbjdJ7w2_yi2_4LcjH7GqokM,86265
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=dvgKn8JCobuvyD7fClF5lcbeHESCvV8gZ_U_lQnYRok,16867
12
+ xslope/search.py,sha256=ZY0Gw14l_V-ORg7HKNHVAGud-peVg4ASdNfv2h0-zoE,17587
13
13
  xslope/seep.py,sha256=AlZvp1kSPBfbNkpSZUNXqaefIRuT6BZPxhadmb5DFsk,93270
14
- xslope/slice.py,sha256=5zUl3qSF4PMt2d9y6jAX881aTcUWVMeVN87wt-BbIao,46380
14
+ xslope/slice.py,sha256=zNbLtaMXdUMVGOgXLFJO-YvVWlsqPyHnDNHpAxzD0dQ,47100
15
15
  xslope/solve.py,sha256=_whlUuSsDqqe0wtgLowucc9dXmC8Q6J9zbViTQOmo-I,49337
16
- xslope-0.1.13.dist-info/LICENSE,sha256=NU5J88FUai_4Ixu5DqOQukA-gcXAyX8pXLhIg8OB3AM,10969
17
- xslope-0.1.13.dist-info/METADATA,sha256=gnmFMyTdek5oNE5MDHkb_WX6hiMKBJ9dP2DdG4OhHYI,2028
18
- xslope-0.1.13.dist-info/NOTICE,sha256=E-sbN0MWwvJC27Z-2_G4VUHIx4IsfvLDTmOstvY4-OQ,530
19
- xslope-0.1.13.dist-info/WHEEL,sha256=beeZ86-EfXScwlR_HKu4SllMC9wUEj_8Z_4FJ3egI2w,91
20
- xslope-0.1.13.dist-info/top_level.txt,sha256=5qHbWJ1R2pdTNIainFyrVtFk8R1tRcwIn0kzTuwuV1Q,7
21
- xslope-0.1.13.dist-info/RECORD,,
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,,