xslope 0.1.12__py3-none-any.whl → 0.1.14__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/advanced.py +3 -3
- xslope/fem.py +3 -3
- xslope/fileio.py +427 -240
- xslope/mesh.py +80 -27
- xslope/plot.py +194 -47
- xslope/plot_seep.py +7 -7
- xslope/seep.py +9 -9
- xslope/slice.py +31 -9
- xslope/solve.py +20 -8
- {xslope-0.1.12.dist-info → xslope-0.1.14.dist-info}/METADATA +1 -1
- xslope-0.1.14.dist-info/RECORD +21 -0
- xslope-0.1.12.dist-info/RECORD +0 -21
- {xslope-0.1.12.dist-info → xslope-0.1.14.dist-info}/LICENSE +0 -0
- {xslope-0.1.12.dist-info → xslope-0.1.14.dist-info}/NOTICE +0 -0
- {xslope-0.1.12.dist-info → xslope-0.1.14.dist-info}/WHEEL +0 -0
- {xslope-0.1.12.dist-info → xslope-0.1.14.dist-info}/top_level.txt +0 -0
xslope/mesh.py
CHANGED
|
@@ -55,13 +55,13 @@ def _get_gmsh():
|
|
|
55
55
|
|
|
56
56
|
|
|
57
57
|
|
|
58
|
-
def build_mesh_from_polygons(polygons, target_size, element_type='tri3', lines=None, debug=False, mesh_params=None, target_size_1d=None):
|
|
58
|
+
def build_mesh_from_polygons(polygons, target_size, element_type='tri3', lines=None, debug=False, mesh_params=None, target_size_1d=None, profile_lines=None):
|
|
59
59
|
"""
|
|
60
60
|
Build a finite element mesh with material regions using Gmsh.
|
|
61
61
|
Fixed version that properly handles shared boundaries between polygons.
|
|
62
62
|
|
|
63
63
|
Parameters:
|
|
64
|
-
polygons : List of
|
|
64
|
+
polygons : List of polygon coordinate lists or dicts with "coords"/"mat_id"
|
|
65
65
|
target_size : Desired element size
|
|
66
66
|
element_type : 'tri3' (3-node triangles), 'tri6' (6-node triangles),
|
|
67
67
|
'quad4' (4-node quadrilaterals), 'quad8' (8-node quadrilaterals),
|
|
@@ -70,6 +70,7 @@ def build_mesh_from_polygons(polygons, target_size, element_type='tri3', lines=N
|
|
|
70
70
|
debug : Enable debug output
|
|
71
71
|
mesh_params : Optional dictionary of GMSH meshing parameters to override defaults
|
|
72
72
|
target_size_1d : Optional target size for 1D elements (default None, which is set to target_size if None)
|
|
73
|
+
profile_lines: Optional list of profile line dicts with 'mat_id' keys for material assignment
|
|
73
74
|
|
|
74
75
|
Returns:
|
|
75
76
|
mesh dict containing:
|
|
@@ -92,8 +93,35 @@ def build_mesh_from_polygons(polygons, target_size, element_type='tri3', lines=N
|
|
|
92
93
|
if debug:
|
|
93
94
|
print(f"Using default target_size_1d = target_size = {target_size_1d}")
|
|
94
95
|
|
|
95
|
-
#
|
|
96
|
-
|
|
96
|
+
# Normalize polygons to coordinate lists and optional mat_id
|
|
97
|
+
polygon_coords = []
|
|
98
|
+
polygon_mat_ids = []
|
|
99
|
+
for i, polygon in enumerate(polygons):
|
|
100
|
+
if isinstance(polygon, dict):
|
|
101
|
+
polygon_coords.append(polygon.get("coords", []))
|
|
102
|
+
polygon_mat_ids.append(polygon.get("mat_id"))
|
|
103
|
+
else:
|
|
104
|
+
polygon_coords.append(polygon)
|
|
105
|
+
polygon_mat_ids.append(None)
|
|
106
|
+
|
|
107
|
+
# Build a list of region ids (list of material IDs - one per polygon)
|
|
108
|
+
if any(mat_id is not None for mat_id in polygon_mat_ids):
|
|
109
|
+
region_ids = [
|
|
110
|
+
mat_id if mat_id is not None else i
|
|
111
|
+
for i, mat_id in enumerate(polygon_mat_ids)
|
|
112
|
+
]
|
|
113
|
+
elif profile_lines and len(profile_lines) >= len(polygon_coords):
|
|
114
|
+
region_ids = []
|
|
115
|
+
for i in range(len(polygon_coords)):
|
|
116
|
+
mat_id = profile_lines[i].get('mat_id')
|
|
117
|
+
if mat_id is not None:
|
|
118
|
+
region_ids.append(mat_id)
|
|
119
|
+
else:
|
|
120
|
+
# Fallback to polygon index if no mat_id
|
|
121
|
+
region_ids.append(i)
|
|
122
|
+
else:
|
|
123
|
+
# Fallback to sequential IDs if no profile_lines provided
|
|
124
|
+
region_ids = [i for i in range(len(polygon_coords))]
|
|
97
125
|
|
|
98
126
|
if element_type not in ['tri3', 'tri6', 'quad4', 'quad8', 'quad9']:
|
|
99
127
|
raise ValueError("element_type must be 'tri3', 'tri6', 'quad4', 'quad8', or 'quad9'")
|
|
@@ -160,7 +188,7 @@ def build_mesh_from_polygons(polygons, target_size, element_type='tri3', lines=N
|
|
|
160
188
|
short_edge_points = set() # Points that are endpoints of short edges
|
|
161
189
|
|
|
162
190
|
# Pre-pass to identify short edges - improved logic
|
|
163
|
-
for idx, (poly_pts, region_id) in enumerate(zip(
|
|
191
|
+
for idx, (poly_pts, region_id) in enumerate(zip(polygon_coords, region_ids)):
|
|
164
192
|
poly_pts_clean = remove_duplicate_endpoint(list(poly_pts))
|
|
165
193
|
for i in range(len(poly_pts_clean)):
|
|
166
194
|
p1 = poly_pts_clean[i]
|
|
@@ -187,7 +215,7 @@ def build_mesh_from_polygons(polygons, target_size, element_type='tri3', lines=N
|
|
|
187
215
|
print(f"Short edge ignored (major boundary): {p1} to {p2}, length={edge_length:.2f}")
|
|
188
216
|
|
|
189
217
|
# Main pass: Create points with appropriate sizes
|
|
190
|
-
for idx, (poly_pts, region_id) in enumerate(zip(
|
|
218
|
+
for idx, (poly_pts, region_id) in enumerate(zip(polygon_coords, region_ids)):
|
|
191
219
|
poly_pts_clean = remove_duplicate_endpoint(list(poly_pts)) # make a copy
|
|
192
220
|
pt_tags = []
|
|
193
221
|
for x, y in poly_pts_clean:
|
|
@@ -1218,7 +1246,9 @@ def build_polygons(slope_data, reinf_lines=None, tol = 0.000001, debug=False):
|
|
|
1218
1246
|
slope_data: Dictionary containing slope geometry data
|
|
1219
1247
|
|
|
1220
1248
|
Returns:
|
|
1221
|
-
List of polygons
|
|
1249
|
+
List of polygons as dicts with keys:
|
|
1250
|
+
"coords": list of (x, y) coordinate tuples
|
|
1251
|
+
"mat_id": optional material ID (0-based) or None
|
|
1222
1252
|
"""
|
|
1223
1253
|
import numpy as np
|
|
1224
1254
|
import copy
|
|
@@ -1236,7 +1266,7 @@ def build_polygons(slope_data, reinf_lines=None, tol = 0.000001, debug=False):
|
|
|
1236
1266
|
raise ValueError("When using only 1 profile line, max_depth must be specified")
|
|
1237
1267
|
|
|
1238
1268
|
n = len(profile_lines)
|
|
1239
|
-
lines = [list(line) for line in copy.deepcopy(profile_lines)]
|
|
1269
|
+
lines = [list(line['coords']) for line in copy.deepcopy(profile_lines)]
|
|
1240
1270
|
|
|
1241
1271
|
for i in range(n - 1):
|
|
1242
1272
|
top = lines[i]
|
|
@@ -1666,7 +1696,11 @@ def build_polygons(slope_data, reinf_lines=None, tol = 0.000001, debug=False):
|
|
|
1666
1696
|
|
|
1667
1697
|
# Clean up polygon (should rarely do anything)
|
|
1668
1698
|
poly = clean_polygon(poly)
|
|
1669
|
-
|
|
1699
|
+
mat_id = profile_lines[i].get("mat_id") if i < len(profile_lines) else None
|
|
1700
|
+
polygons.append({
|
|
1701
|
+
"coords": poly,
|
|
1702
|
+
"mat_id": mat_id
|
|
1703
|
+
})
|
|
1670
1704
|
|
|
1671
1705
|
# Add distributed load points to polygon edges if coincident
|
|
1672
1706
|
polygons = add_dload_points_to_polygons(polygons, slope_data)
|
|
@@ -1683,7 +1717,7 @@ def add_dload_points_to_polygons(polygons, slope_data):
|
|
|
1683
1717
|
but not existing vertices.
|
|
1684
1718
|
|
|
1685
1719
|
Parameters:
|
|
1686
|
-
polygons: List of polygons (
|
|
1720
|
+
polygons: List of polygons (list of (x,y) tuples) or dicts with "coords"
|
|
1687
1721
|
slope_data: Dictionary containing slope data
|
|
1688
1722
|
|
|
1689
1723
|
Returns:
|
|
@@ -1708,7 +1742,8 @@ def add_dload_points_to_polygons(polygons, slope_data):
|
|
|
1708
1742
|
# Process each polygon
|
|
1709
1743
|
updated_polygons = []
|
|
1710
1744
|
for poly in polygons:
|
|
1711
|
-
|
|
1745
|
+
coords = poly.get("coords", []) if isinstance(poly, dict) else poly
|
|
1746
|
+
updated_poly = list(coords) # Make a copy
|
|
1712
1747
|
|
|
1713
1748
|
# Check each point against polygon edges
|
|
1714
1749
|
for check_point in points_to_check:
|
|
@@ -1735,7 +1770,12 @@ def add_dload_points_to_polygons(polygons, slope_data):
|
|
|
1735
1770
|
updated_poly.insert(i + 1, (round(x_check, 6), round(y_check, 6)))
|
|
1736
1771
|
break # Only insert once per point
|
|
1737
1772
|
|
|
1738
|
-
|
|
1773
|
+
if isinstance(poly, dict):
|
|
1774
|
+
updated_entry = dict(poly)
|
|
1775
|
+
updated_entry["coords"] = updated_poly
|
|
1776
|
+
updated_polygons.append(updated_entry)
|
|
1777
|
+
else:
|
|
1778
|
+
updated_polygons.append(updated_poly)
|
|
1739
1779
|
|
|
1740
1780
|
return updated_polygons
|
|
1741
1781
|
|
|
@@ -1786,29 +1826,33 @@ def print_polygon_summary(polygons):
|
|
|
1786
1826
|
Prints a summary of the generated polygons for diagnostic purposes.
|
|
1787
1827
|
|
|
1788
1828
|
Parameters:
|
|
1789
|
-
polygons: List of polygon coordinate lists
|
|
1829
|
+
polygons: List of polygon coordinate lists or dicts with "coords"
|
|
1790
1830
|
"""
|
|
1791
1831
|
print("=== POLYGON SUMMARY ===")
|
|
1792
1832
|
print(f"Number of material zones: {len(polygons)}")
|
|
1793
1833
|
print()
|
|
1794
1834
|
|
|
1795
1835
|
for i, polygon in enumerate(polygons):
|
|
1796
|
-
|
|
1797
|
-
|
|
1836
|
+
coords = polygon.get("coords") if isinstance(polygon, dict) else polygon
|
|
1837
|
+
mat_id = polygon.get("mat_id") if isinstance(polygon, dict) else i
|
|
1838
|
+
if mat_id is None:
|
|
1839
|
+
mat_id = i
|
|
1840
|
+
print(f"Material Zone {i+1} (Material ID: {mat_id}):")
|
|
1841
|
+
print(f" Number of vertices: {len(coords)}")
|
|
1798
1842
|
|
|
1799
1843
|
# Calculate area (simple shoelace formula)
|
|
1800
1844
|
area = 0
|
|
1801
|
-
for j in range(len(
|
|
1802
|
-
x1, y1 =
|
|
1803
|
-
x2, y2 =
|
|
1845
|
+
for j in range(len(coords) - 1):
|
|
1846
|
+
x1, y1 = coords[j]
|
|
1847
|
+
x2, y2 = coords[j + 1]
|
|
1804
1848
|
area += (x2 - x1) * (y2 + y1) / 2
|
|
1805
1849
|
area = abs(area)
|
|
1806
1850
|
|
|
1807
1851
|
print(f" Approximate area: {area:.2f} square units")
|
|
1808
1852
|
|
|
1809
1853
|
# Print bounding box
|
|
1810
|
-
xs = [x for x, y in
|
|
1811
|
-
ys = [y for x, y in
|
|
1854
|
+
xs = [x for x, y in coords]
|
|
1855
|
+
ys = [y for x, y in coords]
|
|
1812
1856
|
print(f" Bounding box: x=[{min(xs):.2f}, {max(xs):.2f}], y=[{min(ys):.2f}, {max(ys):.2f}]")
|
|
1813
1857
|
print()
|
|
1814
1858
|
|
|
@@ -2983,7 +3027,7 @@ def add_intersection_points_to_polygons(polygons, lines, debug=False):
|
|
|
2983
3027
|
This ensures that polygons have vertices at all intersection points with reinforcement lines.
|
|
2984
3028
|
|
|
2985
3029
|
Parameters:
|
|
2986
|
-
polygons: List of polygons (lists of (x,y) tuples)
|
|
3030
|
+
polygons: List of polygons (lists of (x,y) tuples) or dicts with "coords"
|
|
2987
3031
|
lines: List of reinforcement lines (lists of (x,y) tuples)
|
|
2988
3032
|
debug: Enable debug output
|
|
2989
3033
|
|
|
@@ -2999,7 +3043,12 @@ def add_intersection_points_to_polygons(polygons, lines, debug=False):
|
|
|
2999
3043
|
# Make a copy of polygons to modify
|
|
3000
3044
|
updated_polygons = []
|
|
3001
3045
|
for poly in polygons:
|
|
3002
|
-
|
|
3046
|
+
if isinstance(poly, dict):
|
|
3047
|
+
updated_entry = dict(poly)
|
|
3048
|
+
updated_entry["coords"] = list(poly.get("coords", []))
|
|
3049
|
+
updated_polygons.append(updated_entry)
|
|
3050
|
+
else:
|
|
3051
|
+
updated_polygons.append(list(poly)) # Convert to list for modification
|
|
3003
3052
|
|
|
3004
3053
|
# Find all intersections
|
|
3005
3054
|
for line_idx, line_pts in enumerate(lines):
|
|
@@ -3015,10 +3064,11 @@ def add_intersection_points_to_polygons(polygons, lines, debug=False):
|
|
|
3015
3064
|
|
|
3016
3065
|
# Check intersection with each polygon
|
|
3017
3066
|
for poly_idx, poly in enumerate(updated_polygons):
|
|
3067
|
+
poly_coords = poly.get("coords", []) if isinstance(poly, dict) else poly
|
|
3018
3068
|
# Check each edge of this polygon
|
|
3019
|
-
for j in range(len(
|
|
3020
|
-
poly_edge_start =
|
|
3021
|
-
poly_edge_end =
|
|
3069
|
+
for j in range(len(poly_coords)):
|
|
3070
|
+
poly_edge_start = poly_coords[j]
|
|
3071
|
+
poly_edge_end = poly_coords[(j + 1) % len(poly_coords)]
|
|
3022
3072
|
|
|
3023
3073
|
# Find intersection point if it exists
|
|
3024
3074
|
intersection = line_segment_intersection(
|
|
@@ -3032,7 +3082,7 @@ def add_intersection_points_to_polygons(polygons, lines, debug=False):
|
|
|
3032
3082
|
|
|
3033
3083
|
# Check if intersection point is already a vertex of this polygon
|
|
3034
3084
|
is_vertex = False
|
|
3035
|
-
for vertex in
|
|
3085
|
+
for vertex in poly_coords:
|
|
3036
3086
|
if abs(vertex[0] - intersection[0]) < 1e-8 and abs(vertex[1] - intersection[1]) < 1e-8:
|
|
3037
3087
|
is_vertex = True
|
|
3038
3088
|
break
|
|
@@ -3041,7 +3091,10 @@ def add_intersection_points_to_polygons(polygons, lines, debug=False):
|
|
|
3041
3091
|
# Insert intersection point into polygon at the correct position
|
|
3042
3092
|
# Insert after vertex j (which is the start of the edge)
|
|
3043
3093
|
insert_idx = j + 1
|
|
3044
|
-
updated_polygons[poly_idx]
|
|
3094
|
+
if isinstance(updated_polygons[poly_idx], dict):
|
|
3095
|
+
updated_polygons[poly_idx]["coords"].insert(insert_idx, intersection)
|
|
3096
|
+
else:
|
|
3097
|
+
updated_polygons[poly_idx].insert(insert_idx, intersection)
|
|
3045
3098
|
|
|
3046
3099
|
if debug:
|
|
3047
3100
|
print(f"Added intersection point {intersection} to polygon {poly_idx} at position {insert_idx}")
|
xslope/plot.py
CHANGED
|
@@ -54,20 +54,142 @@ def get_dload_legend_handler():
|
|
|
54
54
|
return None, dummy_line
|
|
55
55
|
|
|
56
56
|
|
|
57
|
-
def plot_profile_lines(ax, profile_lines):
|
|
57
|
+
def plot_profile_lines(ax, profile_lines, materials=None, labels=False):
|
|
58
58
|
"""
|
|
59
59
|
Plots the profile lines for each material in the slope.
|
|
60
60
|
|
|
61
61
|
Parameters:
|
|
62
62
|
ax: matplotlib Axes object
|
|
63
|
-
profile_lines: List of line
|
|
63
|
+
profile_lines: List of profile line dicts, each with 'coords' and 'mat_id' keys
|
|
64
|
+
materials: List of material dictionaries (optional, for color mapping)
|
|
65
|
+
labels: If True, add index labels to each profile line (default: False)
|
|
64
66
|
|
|
65
67
|
Returns:
|
|
66
68
|
None
|
|
67
69
|
"""
|
|
68
70
|
for i, line in enumerate(profile_lines):
|
|
69
|
-
|
|
70
|
-
|
|
71
|
+
coords = line['coords']
|
|
72
|
+
xs, ys = zip(*coords)
|
|
73
|
+
|
|
74
|
+
# Get material index from mat_id (already 0-based)
|
|
75
|
+
if materials and line.get('mat_id') is not None:
|
|
76
|
+
mat_idx = line['mat_id']
|
|
77
|
+
if 0 <= mat_idx < len(materials):
|
|
78
|
+
color = get_material_color(mat_idx)
|
|
79
|
+
else:
|
|
80
|
+
# Fallback to index-based color if mat_id out of range
|
|
81
|
+
color = get_material_color(i)
|
|
82
|
+
else:
|
|
83
|
+
# Fallback to index-based color if no materials or mat_id
|
|
84
|
+
color = get_material_color(i)
|
|
85
|
+
|
|
86
|
+
ax.plot(xs, ys, color=color, linewidth=1, label=f'Profile {i+1}')
|
|
87
|
+
|
|
88
|
+
if labels:
|
|
89
|
+
_add_profile_index_label(ax, coords, i + 1, color)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _add_profile_index_label(ax, line, index, color):
|
|
93
|
+
"""
|
|
94
|
+
Adds an index label to a profile line, positioned on a suitable segment.
|
|
95
|
+
|
|
96
|
+
Parameters:
|
|
97
|
+
ax: matplotlib Axes object
|
|
98
|
+
line: List of (x, y) coordinates for the profile line
|
|
99
|
+
index: The index number to display (1-based)
|
|
100
|
+
color: Color for the label text
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
None
|
|
104
|
+
"""
|
|
105
|
+
if len(line) < 2:
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
# Build list of segments with their properties
|
|
109
|
+
segments = []
|
|
110
|
+
for j in range(len(line) - 1):
|
|
111
|
+
x1, y1 = line[j]
|
|
112
|
+
x2, y2 = line[j + 1]
|
|
113
|
+
dx = x2 - x1
|
|
114
|
+
dy = y2 - y1
|
|
115
|
+
length = np.sqrt(dx**2 + dy**2)
|
|
116
|
+
|
|
117
|
+
if length < 1e-9:
|
|
118
|
+
continue
|
|
119
|
+
|
|
120
|
+
# Calculate how horizontal the segment is (1.0 = perfectly horizontal)
|
|
121
|
+
horizontalness = abs(dx) / length
|
|
122
|
+
|
|
123
|
+
# Midpoint of the segment
|
|
124
|
+
mid_x = (x1 + x2) / 2
|
|
125
|
+
mid_y = (y1 + y2) / 2
|
|
126
|
+
|
|
127
|
+
segments.append({
|
|
128
|
+
'length': length,
|
|
129
|
+
'horizontalness': horizontalness,
|
|
130
|
+
'mid_x': mid_x,
|
|
131
|
+
'mid_y': mid_y,
|
|
132
|
+
'position': j # segment index in the line
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
if not segments:
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
# Calculate the total line length and find segments in the middle third
|
|
139
|
+
total_length = sum(s['length'] for s in segments)
|
|
140
|
+
|
|
141
|
+
# Score segments: prefer longer, more horizontal segments near the middle
|
|
142
|
+
# Avoid very short segments (less than 5% of total length)
|
|
143
|
+
min_length_threshold = 0.05 * total_length
|
|
144
|
+
|
|
145
|
+
n_segments = len(segments)
|
|
146
|
+
best_segment = None
|
|
147
|
+
best_score = -1
|
|
148
|
+
|
|
149
|
+
for seg in segments:
|
|
150
|
+
# Skip very short segments
|
|
151
|
+
if seg['length'] < min_length_threshold:
|
|
152
|
+
continue
|
|
153
|
+
|
|
154
|
+
# Calculate how close to the middle this segment is (0-1 scale, 1 = center)
|
|
155
|
+
position_ratio = (seg['position'] + 0.5) / n_segments
|
|
156
|
+
middle_score = 1.0 - 2.0 * abs(position_ratio - 0.5) # 1.0 at center, 0.0 at ends
|
|
157
|
+
|
|
158
|
+
# Score: weight length, horizontalness, and middle position
|
|
159
|
+
# Length is normalized by total length
|
|
160
|
+
length_score = seg['length'] / total_length
|
|
161
|
+
|
|
162
|
+
# Combined score: prioritize horizontalness, then middle position, then length
|
|
163
|
+
score = (seg['horizontalness'] * 2.0 +
|
|
164
|
+
middle_score * 1.5 +
|
|
165
|
+
length_score * 1.0)
|
|
166
|
+
|
|
167
|
+
if score > best_score:
|
|
168
|
+
best_score = score
|
|
169
|
+
best_segment = seg
|
|
170
|
+
|
|
171
|
+
# Fallback: if no segment passed the threshold, use the longest one
|
|
172
|
+
if best_segment is None:
|
|
173
|
+
best_segment = max(segments, key=lambda s: s['length'])
|
|
174
|
+
|
|
175
|
+
# Place the label at the midpoint of the chosen segment
|
|
176
|
+
ax.text(
|
|
177
|
+
best_segment['mid_x'],
|
|
178
|
+
best_segment['mid_y'],
|
|
179
|
+
str(index),
|
|
180
|
+
fontsize=7,
|
|
181
|
+
color=color,
|
|
182
|
+
fontfamily='monospace',
|
|
183
|
+
ha='center',
|
|
184
|
+
va='center',
|
|
185
|
+
bbox=dict(
|
|
186
|
+
boxstyle='round,pad=0.3',
|
|
187
|
+
facecolor='white',
|
|
188
|
+
edgecolor=color,
|
|
189
|
+
linewidth=0.8
|
|
190
|
+
),
|
|
191
|
+
zorder=10
|
|
192
|
+
)
|
|
71
193
|
|
|
72
194
|
def plot_max_depth(ax, profile_lines, max_depth):
|
|
73
195
|
"""
|
|
@@ -75,7 +197,7 @@ def plot_max_depth(ax, profile_lines, max_depth):
|
|
|
75
197
|
|
|
76
198
|
Parameters:
|
|
77
199
|
ax: matplotlib Axes object
|
|
78
|
-
profile_lines: List of line
|
|
200
|
+
profile_lines: List of profile line dicts, each with 'coords' key containing coordinate tuples
|
|
79
201
|
max_depth: Maximum allowed depth for analysis
|
|
80
202
|
|
|
81
203
|
Returns:
|
|
@@ -83,7 +205,7 @@ def plot_max_depth(ax, profile_lines, max_depth):
|
|
|
83
205
|
"""
|
|
84
206
|
if max_depth is None:
|
|
85
207
|
return
|
|
86
|
-
x_vals = [x for line in profile_lines for x, _ in line]
|
|
208
|
+
x_vals = [x for line in profile_lines for x, _ in line['coords']]
|
|
87
209
|
x_min = min(x_vals)
|
|
88
210
|
x_max = max(x_vals)
|
|
89
211
|
ax.hlines(max_depth, x_min, x_max, colors='black', linewidth=1.5, label='Max Depth')
|
|
@@ -220,7 +342,7 @@ def plot_piezo_line(ax, slope_data):
|
|
|
220
342
|
|
|
221
343
|
def plot_seepage_bc_lines(ax, slope_data):
|
|
222
344
|
"""
|
|
223
|
-
Plots
|
|
345
|
+
Plots seep boundary-condition lines for seep-only workflows.
|
|
224
346
|
|
|
225
347
|
- Specified head geometry: solid dark blue, thicker than profile lines
|
|
226
348
|
- Exit face geometry: solid red
|
|
@@ -245,7 +367,7 @@ def plot_seepage_bc_lines(ax, slope_data):
|
|
|
245
367
|
x_vals = []
|
|
246
368
|
for line in slope_data.get("profile_lines", []):
|
|
247
369
|
try:
|
|
248
|
-
xs_line, _ = zip(*line)
|
|
370
|
+
xs_line, _ = zip(*line['coords'])
|
|
249
371
|
x_vals.extend(xs_line)
|
|
250
372
|
except Exception:
|
|
251
373
|
pass
|
|
@@ -551,11 +673,16 @@ def plot_circles(ax, slope_data):
|
|
|
551
673
|
dx /= length
|
|
552
674
|
dy /= length
|
|
553
675
|
|
|
554
|
-
#
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
676
|
+
# Draw arrow with pixel-based head size
|
|
677
|
+
ax.annotate('',
|
|
678
|
+
xy=(Xo + dx * R, Yo + dy * R), # arrow tip
|
|
679
|
+
xytext=(Xo, Yo), # arrow start
|
|
680
|
+
arrowprops=dict(
|
|
681
|
+
arrowstyle='-|>',
|
|
682
|
+
color='red',
|
|
683
|
+
lw=1.0, # shaft width in points
|
|
684
|
+
mutation_scale=20 # head size in points
|
|
685
|
+
))
|
|
559
686
|
|
|
560
687
|
def plot_non_circ(ax, non_circ):
|
|
561
688
|
"""
|
|
@@ -687,14 +814,14 @@ def plot_lem_material_table(ax, materials, xloc=0.6, yloc=0.7):
|
|
|
687
814
|
|
|
688
815
|
def plot_seep_material_table(ax, seep_data, xloc=0.6, yloc=0.7):
|
|
689
816
|
"""
|
|
690
|
-
Adds a
|
|
817
|
+
Adds a seep material properties table to the plot.
|
|
691
818
|
|
|
692
|
-
Displays hydraulic properties for
|
|
819
|
+
Displays hydraulic properties for seep analysis including hydraulic conductivities
|
|
693
820
|
(k₁, k₂), anisotropy angle, and unsaturated flow parameters (kr₀, h₀).
|
|
694
821
|
|
|
695
822
|
Parameters:
|
|
696
823
|
ax: matplotlib Axes object to add the table to
|
|
697
|
-
seep_data: Dictionary containing
|
|
824
|
+
seep_data: Dictionary containing seep material properties with keys:
|
|
698
825
|
- 'k1_by_mat': List of primary hydraulic conductivity values (float)
|
|
699
826
|
- 'k2_by_mat': List of secondary hydraulic conductivity values (float)
|
|
700
827
|
- 'angle_by_mat': List of anisotropy angles in degrees (float)
|
|
@@ -1036,10 +1163,11 @@ def compute_ylim(data, slice_df, scale_frac=0.5, pad_fraction=0.1):
|
|
|
1036
1163
|
|
|
1037
1164
|
# 1) collect all profile line elevations
|
|
1038
1165
|
for line in data.get('profile_lines', []):
|
|
1039
|
-
|
|
1040
|
-
|
|
1166
|
+
coords = line['coords']
|
|
1167
|
+
if hasattr(coords, "xy"):
|
|
1168
|
+
_, ys = coords.xy
|
|
1041
1169
|
else:
|
|
1042
|
-
_, ys = zip(*
|
|
1170
|
+
_, ys = zip(*coords)
|
|
1043
1171
|
y_vals.extend(ys)
|
|
1044
1172
|
|
|
1045
1173
|
# 2) explicitly include the deepest allowed depth
|
|
@@ -1108,11 +1236,11 @@ def plot_inputs(
|
|
|
1108
1236
|
slope_data,
|
|
1109
1237
|
title="Slope Geometry and Inputs",
|
|
1110
1238
|
figsize=(12, 6),
|
|
1111
|
-
mat_table=
|
|
1239
|
+
mat_table=False,
|
|
1112
1240
|
save_png=False,
|
|
1113
1241
|
dpi=300,
|
|
1114
1242
|
mode="lem",
|
|
1115
|
-
tab_loc="
|
|
1243
|
+
tab_loc="top",
|
|
1116
1244
|
legend_ncol="auto",
|
|
1117
1245
|
legend_max_cols=6,
|
|
1118
1246
|
legend_max_rows=4,
|
|
@@ -1158,7 +1286,7 @@ def plot_inputs(
|
|
|
1158
1286
|
fig, ax = plt.subplots(figsize=figsize)
|
|
1159
1287
|
|
|
1160
1288
|
# Plot contents
|
|
1161
|
-
plot_profile_lines(ax, slope_data['profile_lines'])
|
|
1289
|
+
plot_profile_lines(ax, slope_data['profile_lines'], materials=slope_data.get('materials'), labels=True)
|
|
1162
1290
|
plot_max_depth(ax, slope_data['profile_lines'], slope_data['max_depth'])
|
|
1163
1291
|
plot_piezo_line(ax, slope_data)
|
|
1164
1292
|
if mode == "seep":
|
|
@@ -1358,7 +1486,7 @@ def plot_solution(slope_data, slice_df, failure_surface, results, figsize=(12, 7
|
|
|
1358
1486
|
ax.set_ylabel("y")
|
|
1359
1487
|
ax.grid(False)
|
|
1360
1488
|
|
|
1361
|
-
plot_profile_lines(ax, slope_data['profile_lines'])
|
|
1489
|
+
plot_profile_lines(ax, slope_data['profile_lines'], materials=slope_data.get('materials'))
|
|
1362
1490
|
plot_max_depth(ax, slope_data['profile_lines'], slope_data['max_depth'])
|
|
1363
1491
|
plot_slices(ax, slice_df, fill=False)
|
|
1364
1492
|
plot_failure_surface(ax, failure_surface)
|
|
@@ -1510,7 +1638,7 @@ def plot_circular_search_results(slope_data, fs_cache, search_path=None, highlig
|
|
|
1510
1638
|
"""
|
|
1511
1639
|
fig, ax = plt.subplots(figsize=figsize)
|
|
1512
1640
|
|
|
1513
|
-
plot_profile_lines(ax, slope_data['profile_lines'])
|
|
1641
|
+
plot_profile_lines(ax, slope_data['profile_lines'], materials=slope_data.get('materials'))
|
|
1514
1642
|
plot_max_depth(ax, slope_data['profile_lines'], slope_data['max_depth'])
|
|
1515
1643
|
plot_piezo_line(ax, slope_data)
|
|
1516
1644
|
plot_dloads(ax, slope_data)
|
|
@@ -1556,7 +1684,7 @@ def plot_noncircular_search_results(slope_data, fs_cache, search_path=None, high
|
|
|
1556
1684
|
fig, ax = plt.subplots(figsize=figsize)
|
|
1557
1685
|
|
|
1558
1686
|
# Plot basic profile elements
|
|
1559
|
-
plot_profile_lines(ax, slope_data['profile_lines'])
|
|
1687
|
+
plot_profile_lines(ax, slope_data['profile_lines'], materials=slope_data.get('materials'))
|
|
1560
1688
|
plot_max_depth(ax, slope_data['profile_lines'], slope_data['max_depth'])
|
|
1561
1689
|
plot_piezo_line(ax, slope_data)
|
|
1562
1690
|
plot_dloads(ax, slope_data)
|
|
@@ -1623,7 +1751,7 @@ def plot_reliability_results(slope_data, reliability_data, figsize=(12, 7), save
|
|
|
1623
1751
|
fig, ax = plt.subplots(figsize=figsize)
|
|
1624
1752
|
|
|
1625
1753
|
# Plot basic slope elements (same as other search functions)
|
|
1626
|
-
plot_profile_lines(ax, slope_data['profile_lines'])
|
|
1754
|
+
plot_profile_lines(ax, slope_data['profile_lines'], materials=slope_data.get('materials'))
|
|
1627
1755
|
plot_max_depth(ax, slope_data['profile_lines'], slope_data['max_depth'])
|
|
1628
1756
|
plot_piezo_line(ax, slope_data)
|
|
1629
1757
|
plot_dloads(ax, slope_data)
|
|
@@ -1890,34 +2018,48 @@ def plot_mesh(mesh, materials=None, figsize=(14, 6), pad_frac=0.05, show_nodes=T
|
|
|
1890
2018
|
plt.show()
|
|
1891
2019
|
|
|
1892
2020
|
|
|
1893
|
-
def plot_polygons(
|
|
2021
|
+
def plot_polygons(
|
|
2022
|
+
polygons,
|
|
2023
|
+
materials=None,
|
|
2024
|
+
nodes=False,
|
|
2025
|
+
legend=True,
|
|
2026
|
+
title="Material Zone Polygons",
|
|
2027
|
+
figsize=(10, 6),
|
|
2028
|
+
save_png=False,
|
|
2029
|
+
dpi=300,
|
|
2030
|
+
):
|
|
1894
2031
|
"""
|
|
1895
2032
|
Plot all material zone polygons in a single figure.
|
|
1896
2033
|
|
|
1897
2034
|
Parameters:
|
|
1898
|
-
polygons: List of polygon coordinate lists
|
|
2035
|
+
polygons: List of polygon coordinate lists or dicts with "coords"/"mat_id"
|
|
1899
2036
|
materials: Optional list of material dicts (with key "name") or list of material
|
|
1900
2037
|
name strings. If provided, the material name will be used in the legend.
|
|
1901
2038
|
nodes: If True, plot each polygon vertex as a dot.
|
|
1902
2039
|
legend: If True, show the legend.
|
|
1903
2040
|
title: Plot title
|
|
2041
|
+
figsize: Matplotlib figure size tuple, e.g. (10, 6)
|
|
1904
2042
|
"""
|
|
1905
2043
|
import matplotlib.pyplot as plt
|
|
1906
2044
|
|
|
1907
|
-
fig, ax = plt.subplots(figsize=
|
|
2045
|
+
fig, ax = plt.subplots(figsize=figsize)
|
|
1908
2046
|
for i, polygon in enumerate(polygons):
|
|
1909
|
-
|
|
1910
|
-
|
|
2047
|
+
coords = polygon.get("coords", []) if isinstance(polygon, dict) else polygon
|
|
2048
|
+
xs = [x for x, y in coords]
|
|
2049
|
+
ys = [y for x, y in coords]
|
|
2050
|
+
mat_idx = polygon.get("mat_id") if isinstance(polygon, dict) else i
|
|
2051
|
+
if mat_idx is None:
|
|
2052
|
+
mat_idx = i
|
|
1911
2053
|
mat_name = None
|
|
1912
|
-
if materials is not None and
|
|
1913
|
-
item = materials[
|
|
2054
|
+
if materials is not None and 0 <= mat_idx < len(materials):
|
|
2055
|
+
item = materials[mat_idx]
|
|
1914
2056
|
if isinstance(item, dict):
|
|
1915
2057
|
mat_name = item.get("name", None)
|
|
1916
2058
|
elif isinstance(item, str):
|
|
1917
2059
|
mat_name = item
|
|
1918
|
-
label = mat_name if mat_name else f"Material {
|
|
1919
|
-
ax.fill(xs, ys, color=get_material_color(
|
|
1920
|
-
ax.plot(xs, ys, color=get_material_color(
|
|
2060
|
+
label = mat_name if mat_name else f"Material {mat_idx}"
|
|
2061
|
+
ax.fill(xs, ys, color=get_material_color(mat_idx), alpha=0.6, label=label)
|
|
2062
|
+
ax.plot(xs, ys, color=get_material_color(mat_idx), linewidth=1)
|
|
1921
2063
|
if nodes:
|
|
1922
2064
|
# Avoid legend clutter by not adding a label here.
|
|
1923
2065
|
ax.scatter(xs, ys, color='k', s=30, marker='o', zorder=3)
|
|
@@ -1942,7 +2084,7 @@ def plot_polygons_separately(polygons, materials=None, save_png=False, dpi=300):
|
|
|
1942
2084
|
Plot each polygon in a separate matplotlib frame (subplot), with vertices as round dots.
|
|
1943
2085
|
|
|
1944
2086
|
Parameters:
|
|
1945
|
-
polygons: List of polygon coordinate lists
|
|
2087
|
+
polygons: List of polygon coordinate lists or dicts with "coords"/"mat_id"
|
|
1946
2088
|
materials: Optional list of material dicts (with key "name") or list of material
|
|
1947
2089
|
name strings. If provided, the material name will be included in each subplot title.
|
|
1948
2090
|
"""
|
|
@@ -1951,25 +2093,29 @@ def plot_polygons_separately(polygons, materials=None, save_png=False, dpi=300):
|
|
|
1951
2093
|
n = len(polygons)
|
|
1952
2094
|
fig, axes = plt.subplots(n, 1, figsize=(8, 3 * n), squeeze=False)
|
|
1953
2095
|
for i, polygon in enumerate(polygons):
|
|
1954
|
-
|
|
1955
|
-
|
|
2096
|
+
coords = polygon.get("coords", []) if isinstance(polygon, dict) else polygon
|
|
2097
|
+
xs = [x for x, y in coords]
|
|
2098
|
+
ys = [y for x, y in coords]
|
|
1956
2099
|
ax = axes[i, 0]
|
|
1957
|
-
|
|
1958
|
-
|
|
2100
|
+
mat_idx = polygon.get("mat_id") if isinstance(polygon, dict) else i
|
|
2101
|
+
if mat_idx is None:
|
|
2102
|
+
mat_idx = i
|
|
2103
|
+
ax.fill(xs, ys, color=get_material_color(mat_idx), alpha=0.6, label=f'Material {mat_idx}')
|
|
2104
|
+
ax.plot(xs, ys, color=get_material_color(mat_idx), linewidth=1)
|
|
1959
2105
|
ax.scatter(xs, ys, color='k', s=30, marker='o', zorder=3, label='Vertices')
|
|
1960
2106
|
ax.set_xlabel('X Coordinate')
|
|
1961
2107
|
ax.set_ylabel('Y Coordinate')
|
|
1962
2108
|
mat_name = None
|
|
1963
|
-
if materials is not None and
|
|
1964
|
-
item = materials[
|
|
2109
|
+
if materials is not None and 0 <= mat_idx < len(materials):
|
|
2110
|
+
item = materials[mat_idx]
|
|
1965
2111
|
if isinstance(item, dict):
|
|
1966
2112
|
mat_name = item.get("name", None)
|
|
1967
2113
|
elif isinstance(item, str):
|
|
1968
2114
|
mat_name = item
|
|
1969
2115
|
if mat_name:
|
|
1970
|
-
ax.set_title(f'Material {
|
|
2116
|
+
ax.set_title(f'Material {mat_idx}: {mat_name}')
|
|
1971
2117
|
else:
|
|
1972
|
-
ax.set_title(f'Material {
|
|
2118
|
+
ax.set_title(f'Material {mat_idx}')
|
|
1973
2119
|
ax.grid(True, alpha=0.3)
|
|
1974
2120
|
ax.set_aspect('equal')
|
|
1975
2121
|
# Intentionally no legend: these plots are typically used for debugging geometry,
|
|
@@ -2058,8 +2204,9 @@ def get_plot_elements_bounds(ax, slope_data):
|
|
|
2058
2204
|
if 'profile_lines' in slope_data:
|
|
2059
2205
|
for line in slope_data['profile_lines']:
|
|
2060
2206
|
if line:
|
|
2061
|
-
|
|
2062
|
-
|
|
2207
|
+
coords = line['coords']
|
|
2208
|
+
xs = [p[0] for p in coords]
|
|
2209
|
+
ys = [p[1] for p in coords]
|
|
2063
2210
|
bounds.append((min(xs), max(xs), min(ys), max(ys)))
|
|
2064
2211
|
|
|
2065
2212
|
# Distributed loads bounds
|