xslope 0.1.12__py3-none-any.whl → 0.1.13__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 +182 -40
- 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.13.dist-info}/METADATA +1 -1
- xslope-0.1.13.dist-info/RECORD +21 -0
- xslope-0.1.12.dist-info/RECORD +0 -21
- {xslope-0.1.12.dist-info → xslope-0.1.13.dist-info}/LICENSE +0 -0
- {xslope-0.1.12.dist-info → xslope-0.1.13.dist-info}/NOTICE +0 -0
- {xslope-0.1.12.dist-info → xslope-0.1.13.dist-info}/WHEEL +0 -0
- {xslope-0.1.12.dist-info → xslope-0.1.13.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
|
|
@@ -687,14 +809,14 @@ def plot_lem_material_table(ax, materials, xloc=0.6, yloc=0.7):
|
|
|
687
809
|
|
|
688
810
|
def plot_seep_material_table(ax, seep_data, xloc=0.6, yloc=0.7):
|
|
689
811
|
"""
|
|
690
|
-
Adds a
|
|
812
|
+
Adds a seep material properties table to the plot.
|
|
691
813
|
|
|
692
|
-
Displays hydraulic properties for
|
|
814
|
+
Displays hydraulic properties for seep analysis including hydraulic conductivities
|
|
693
815
|
(k₁, k₂), anisotropy angle, and unsaturated flow parameters (kr₀, h₀).
|
|
694
816
|
|
|
695
817
|
Parameters:
|
|
696
818
|
ax: matplotlib Axes object to add the table to
|
|
697
|
-
seep_data: Dictionary containing
|
|
819
|
+
seep_data: Dictionary containing seep material properties with keys:
|
|
698
820
|
- 'k1_by_mat': List of primary hydraulic conductivity values (float)
|
|
699
821
|
- 'k2_by_mat': List of secondary hydraulic conductivity values (float)
|
|
700
822
|
- 'angle_by_mat': List of anisotropy angles in degrees (float)
|
|
@@ -1036,10 +1158,11 @@ def compute_ylim(data, slice_df, scale_frac=0.5, pad_fraction=0.1):
|
|
|
1036
1158
|
|
|
1037
1159
|
# 1) collect all profile line elevations
|
|
1038
1160
|
for line in data.get('profile_lines', []):
|
|
1039
|
-
|
|
1040
|
-
|
|
1161
|
+
coords = line['coords']
|
|
1162
|
+
if hasattr(coords, "xy"):
|
|
1163
|
+
_, ys = coords.xy
|
|
1041
1164
|
else:
|
|
1042
|
-
_, ys = zip(*
|
|
1165
|
+
_, ys = zip(*coords)
|
|
1043
1166
|
y_vals.extend(ys)
|
|
1044
1167
|
|
|
1045
1168
|
# 2) explicitly include the deepest allowed depth
|
|
@@ -1158,7 +1281,7 @@ def plot_inputs(
|
|
|
1158
1281
|
fig, ax = plt.subplots(figsize=figsize)
|
|
1159
1282
|
|
|
1160
1283
|
# Plot contents
|
|
1161
|
-
plot_profile_lines(ax, slope_data['profile_lines'])
|
|
1284
|
+
plot_profile_lines(ax, slope_data['profile_lines'], materials=slope_data.get('materials'), labels=True)
|
|
1162
1285
|
plot_max_depth(ax, slope_data['profile_lines'], slope_data['max_depth'])
|
|
1163
1286
|
plot_piezo_line(ax, slope_data)
|
|
1164
1287
|
if mode == "seep":
|
|
@@ -1358,7 +1481,7 @@ def plot_solution(slope_data, slice_df, failure_surface, results, figsize=(12, 7
|
|
|
1358
1481
|
ax.set_ylabel("y")
|
|
1359
1482
|
ax.grid(False)
|
|
1360
1483
|
|
|
1361
|
-
plot_profile_lines(ax, slope_data['profile_lines'])
|
|
1484
|
+
plot_profile_lines(ax, slope_data['profile_lines'], materials=slope_data.get('materials'))
|
|
1362
1485
|
plot_max_depth(ax, slope_data['profile_lines'], slope_data['max_depth'])
|
|
1363
1486
|
plot_slices(ax, slice_df, fill=False)
|
|
1364
1487
|
plot_failure_surface(ax, failure_surface)
|
|
@@ -1510,7 +1633,7 @@ def plot_circular_search_results(slope_data, fs_cache, search_path=None, highlig
|
|
|
1510
1633
|
"""
|
|
1511
1634
|
fig, ax = plt.subplots(figsize=figsize)
|
|
1512
1635
|
|
|
1513
|
-
plot_profile_lines(ax, slope_data['profile_lines'])
|
|
1636
|
+
plot_profile_lines(ax, slope_data['profile_lines'], materials=slope_data.get('materials'))
|
|
1514
1637
|
plot_max_depth(ax, slope_data['profile_lines'], slope_data['max_depth'])
|
|
1515
1638
|
plot_piezo_line(ax, slope_data)
|
|
1516
1639
|
plot_dloads(ax, slope_data)
|
|
@@ -1556,7 +1679,7 @@ def plot_noncircular_search_results(slope_data, fs_cache, search_path=None, high
|
|
|
1556
1679
|
fig, ax = plt.subplots(figsize=figsize)
|
|
1557
1680
|
|
|
1558
1681
|
# Plot basic profile elements
|
|
1559
|
-
plot_profile_lines(ax, slope_data['profile_lines'])
|
|
1682
|
+
plot_profile_lines(ax, slope_data['profile_lines'], materials=slope_data.get('materials'))
|
|
1560
1683
|
plot_max_depth(ax, slope_data['profile_lines'], slope_data['max_depth'])
|
|
1561
1684
|
plot_piezo_line(ax, slope_data)
|
|
1562
1685
|
plot_dloads(ax, slope_data)
|
|
@@ -1623,7 +1746,7 @@ def plot_reliability_results(slope_data, reliability_data, figsize=(12, 7), save
|
|
|
1623
1746
|
fig, ax = plt.subplots(figsize=figsize)
|
|
1624
1747
|
|
|
1625
1748
|
# Plot basic slope elements (same as other search functions)
|
|
1626
|
-
plot_profile_lines(ax, slope_data['profile_lines'])
|
|
1749
|
+
plot_profile_lines(ax, slope_data['profile_lines'], materials=slope_data.get('materials'))
|
|
1627
1750
|
plot_max_depth(ax, slope_data['profile_lines'], slope_data['max_depth'])
|
|
1628
1751
|
plot_piezo_line(ax, slope_data)
|
|
1629
1752
|
plot_dloads(ax, slope_data)
|
|
@@ -1890,34 +2013,48 @@ def plot_mesh(mesh, materials=None, figsize=(14, 6), pad_frac=0.05, show_nodes=T
|
|
|
1890
2013
|
plt.show()
|
|
1891
2014
|
|
|
1892
2015
|
|
|
1893
|
-
def plot_polygons(
|
|
2016
|
+
def plot_polygons(
|
|
2017
|
+
polygons,
|
|
2018
|
+
materials=None,
|
|
2019
|
+
nodes=False,
|
|
2020
|
+
legend=True,
|
|
2021
|
+
title="Material Zone Polygons",
|
|
2022
|
+
figsize=(10, 6),
|
|
2023
|
+
save_png=False,
|
|
2024
|
+
dpi=300,
|
|
2025
|
+
):
|
|
1894
2026
|
"""
|
|
1895
2027
|
Plot all material zone polygons in a single figure.
|
|
1896
2028
|
|
|
1897
2029
|
Parameters:
|
|
1898
|
-
polygons: List of polygon coordinate lists
|
|
2030
|
+
polygons: List of polygon coordinate lists or dicts with "coords"/"mat_id"
|
|
1899
2031
|
materials: Optional list of material dicts (with key "name") or list of material
|
|
1900
2032
|
name strings. If provided, the material name will be used in the legend.
|
|
1901
2033
|
nodes: If True, plot each polygon vertex as a dot.
|
|
1902
2034
|
legend: If True, show the legend.
|
|
1903
2035
|
title: Plot title
|
|
2036
|
+
figsize: Matplotlib figure size tuple, e.g. (10, 6)
|
|
1904
2037
|
"""
|
|
1905
2038
|
import matplotlib.pyplot as plt
|
|
1906
2039
|
|
|
1907
|
-
fig, ax = plt.subplots(figsize=
|
|
2040
|
+
fig, ax = plt.subplots(figsize=figsize)
|
|
1908
2041
|
for i, polygon in enumerate(polygons):
|
|
1909
|
-
|
|
1910
|
-
|
|
2042
|
+
coords = polygon.get("coords", []) if isinstance(polygon, dict) else polygon
|
|
2043
|
+
xs = [x for x, y in coords]
|
|
2044
|
+
ys = [y for x, y in coords]
|
|
2045
|
+
mat_idx = polygon.get("mat_id") if isinstance(polygon, dict) else i
|
|
2046
|
+
if mat_idx is None:
|
|
2047
|
+
mat_idx = i
|
|
1911
2048
|
mat_name = None
|
|
1912
|
-
if materials is not None and
|
|
1913
|
-
item = materials[
|
|
2049
|
+
if materials is not None and 0 <= mat_idx < len(materials):
|
|
2050
|
+
item = materials[mat_idx]
|
|
1914
2051
|
if isinstance(item, dict):
|
|
1915
2052
|
mat_name = item.get("name", None)
|
|
1916
2053
|
elif isinstance(item, str):
|
|
1917
2054
|
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(
|
|
2055
|
+
label = mat_name if mat_name else f"Material {mat_idx}"
|
|
2056
|
+
ax.fill(xs, ys, color=get_material_color(mat_idx), alpha=0.6, label=label)
|
|
2057
|
+
ax.plot(xs, ys, color=get_material_color(mat_idx), linewidth=1)
|
|
1921
2058
|
if nodes:
|
|
1922
2059
|
# Avoid legend clutter by not adding a label here.
|
|
1923
2060
|
ax.scatter(xs, ys, color='k', s=30, marker='o', zorder=3)
|
|
@@ -1942,7 +2079,7 @@ def plot_polygons_separately(polygons, materials=None, save_png=False, dpi=300):
|
|
|
1942
2079
|
Plot each polygon in a separate matplotlib frame (subplot), with vertices as round dots.
|
|
1943
2080
|
|
|
1944
2081
|
Parameters:
|
|
1945
|
-
polygons: List of polygon coordinate lists
|
|
2082
|
+
polygons: List of polygon coordinate lists or dicts with "coords"/"mat_id"
|
|
1946
2083
|
materials: Optional list of material dicts (with key "name") or list of material
|
|
1947
2084
|
name strings. If provided, the material name will be included in each subplot title.
|
|
1948
2085
|
"""
|
|
@@ -1951,25 +2088,29 @@ def plot_polygons_separately(polygons, materials=None, save_png=False, dpi=300):
|
|
|
1951
2088
|
n = len(polygons)
|
|
1952
2089
|
fig, axes = plt.subplots(n, 1, figsize=(8, 3 * n), squeeze=False)
|
|
1953
2090
|
for i, polygon in enumerate(polygons):
|
|
1954
|
-
|
|
1955
|
-
|
|
2091
|
+
coords = polygon.get("coords", []) if isinstance(polygon, dict) else polygon
|
|
2092
|
+
xs = [x for x, y in coords]
|
|
2093
|
+
ys = [y for x, y in coords]
|
|
1956
2094
|
ax = axes[i, 0]
|
|
1957
|
-
|
|
1958
|
-
|
|
2095
|
+
mat_idx = polygon.get("mat_id") if isinstance(polygon, dict) else i
|
|
2096
|
+
if mat_idx is None:
|
|
2097
|
+
mat_idx = i
|
|
2098
|
+
ax.fill(xs, ys, color=get_material_color(mat_idx), alpha=0.6, label=f'Material {mat_idx}')
|
|
2099
|
+
ax.plot(xs, ys, color=get_material_color(mat_idx), linewidth=1)
|
|
1959
2100
|
ax.scatter(xs, ys, color='k', s=30, marker='o', zorder=3, label='Vertices')
|
|
1960
2101
|
ax.set_xlabel('X Coordinate')
|
|
1961
2102
|
ax.set_ylabel('Y Coordinate')
|
|
1962
2103
|
mat_name = None
|
|
1963
|
-
if materials is not None and
|
|
1964
|
-
item = materials[
|
|
2104
|
+
if materials is not None and 0 <= mat_idx < len(materials):
|
|
2105
|
+
item = materials[mat_idx]
|
|
1965
2106
|
if isinstance(item, dict):
|
|
1966
2107
|
mat_name = item.get("name", None)
|
|
1967
2108
|
elif isinstance(item, str):
|
|
1968
2109
|
mat_name = item
|
|
1969
2110
|
if mat_name:
|
|
1970
|
-
ax.set_title(f'Material {
|
|
2111
|
+
ax.set_title(f'Material {mat_idx}: {mat_name}')
|
|
1971
2112
|
else:
|
|
1972
|
-
ax.set_title(f'Material {
|
|
2113
|
+
ax.set_title(f'Material {mat_idx}')
|
|
1973
2114
|
ax.grid(True, alpha=0.3)
|
|
1974
2115
|
ax.set_aspect('equal')
|
|
1975
2116
|
# Intentionally no legend: these plots are typically used for debugging geometry,
|
|
@@ -2058,8 +2199,9 @@ def get_plot_elements_bounds(ax, slope_data):
|
|
|
2058
2199
|
if 'profile_lines' in slope_data:
|
|
2059
2200
|
for line in slope_data['profile_lines']:
|
|
2060
2201
|
if line:
|
|
2061
|
-
|
|
2062
|
-
|
|
2202
|
+
coords = line['coords']
|
|
2203
|
+
xs = [p[0] for p in coords]
|
|
2204
|
+
ys = [p[1] for p in coords]
|
|
2063
2205
|
bounds.append((min(xs), max(xs), min(ys), max(ys)))
|
|
2064
2206
|
|
|
2065
2207
|
# Distributed loads bounds
|
xslope/plot_seep.py
CHANGED
|
@@ -10,7 +10,7 @@ def plot_seep_data(seep_data, figsize=(14, 6), show_nodes=False, show_bc=False,
|
|
|
10
10
|
Supports both triangular and quadrilateral elements.
|
|
11
11
|
|
|
12
12
|
Args:
|
|
13
|
-
seep_data: Dictionary containing
|
|
13
|
+
seep_data: Dictionary containing seep data from import_seep2d
|
|
14
14
|
show_nodes: If True, plot node points
|
|
15
15
|
show_bc: If True, plot boundary condition nodes
|
|
16
16
|
label_elements: If True, label each element with its number at its centroid
|
|
@@ -206,11 +206,11 @@ def plot_seep_data(seep_data, figsize=(14, 6), show_nodes=False, show_bc=False,
|
|
|
206
206
|
num_triangles = np.sum(element_types == 3)
|
|
207
207
|
num_quads = np.sum(element_types == 4)
|
|
208
208
|
if num_triangles > 0 and num_quads > 0:
|
|
209
|
-
title = f"
|
|
209
|
+
title = f"Finite Element Mesh with Material Zones ({num_triangles} triangles, {num_quads} quads)"
|
|
210
210
|
elif num_quads > 0:
|
|
211
|
-
title = f"
|
|
211
|
+
title = f"Finite Element Mesh with Material Zones ({num_quads} quadrilaterals)"
|
|
212
212
|
else:
|
|
213
|
-
title = f"
|
|
213
|
+
title = f"Finite Element Mesh with Material Zones ({num_triangles} triangles)"
|
|
214
214
|
|
|
215
215
|
ax.set_title(title)
|
|
216
216
|
# plt.subplots_adjust(bottom=0.2) # Add vertical cushion
|
|
@@ -225,9 +225,9 @@ def plot_seep_data(seep_data, figsize=(14, 6), show_nodes=False, show_bc=False,
|
|
|
225
225
|
|
|
226
226
|
def plot_seep_solution(seep_data, solution, figsize=(14, 6), levels=20, base_mat=1, fill_contours=True, phreatic=True, alpha=0.4, pad_frac=0.05, mesh=True, variable="head", vectors=False, vector_scale=0.05, flowlines=True, save_png=False, dpi=300):
|
|
227
227
|
"""
|
|
228
|
-
Plot
|
|
228
|
+
Plot seep analysis results including head contours, flowlines, and phreatic surface.
|
|
229
229
|
|
|
230
|
-
This function visualizes the results of a
|
|
230
|
+
This function visualizes the results of a seep analysis by plotting contours of various
|
|
231
231
|
nodal variables (head, pore pressure, velocity magnitude, or gradient magnitude). When
|
|
232
232
|
plotting head, flowlines are also overlaid. The plot properly handles mesh aspect ratios
|
|
233
233
|
and supports both linear and quadratic triangular and quadrilateral elements.
|
|
@@ -235,7 +235,7 @@ def plot_seep_solution(seep_data, solution, figsize=(14, 6), levels=20, base_mat
|
|
|
235
235
|
Parameters:
|
|
236
236
|
-----------
|
|
237
237
|
seep_data : dict
|
|
238
|
-
Dictionary containing
|
|
238
|
+
Dictionary containing seep mesh data from import_seep2d. Required keys include:
|
|
239
239
|
'nodes', 'elements', 'element_materials', 'element_types' (optional), and
|
|
240
240
|
'k1_by_mat' (optional, for flowline calculation).
|
|
241
241
|
solution : dict
|