xslope 0.1.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- xslope/__init__.py +1 -0
- xslope/_version.py +4 -0
- xslope/advanced.py +460 -0
- xslope/fem.py +2753 -0
- xslope/fileio.py +671 -0
- xslope/global_config.py +59 -0
- xslope/mesh.py +2719 -0
- xslope/plot.py +1484 -0
- xslope/plot_fem.py +1658 -0
- xslope/plot_seep.py +634 -0
- xslope/search.py +416 -0
- xslope/seep.py +2080 -0
- xslope/slice.py +1075 -0
- xslope/solve.py +1259 -0
- xslope-0.1.2.dist-info/LICENSE +196 -0
- xslope-0.1.2.dist-info/METADATA +56 -0
- xslope-0.1.2.dist-info/NOTICE +14 -0
- xslope-0.1.2.dist-info/RECORD +20 -0
- xslope-0.1.2.dist-info/WHEEL +5 -0
- xslope-0.1.2.dist-info/top_level.txt +1 -0
xslope/plot_seep.py
ADDED
|
@@ -0,0 +1,634 @@
|
|
|
1
|
+
import matplotlib.pyplot as plt
|
|
2
|
+
import matplotlib.tri as tri
|
|
3
|
+
from matplotlib.ticker import MaxNLocator
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def plot_seep_data(seep_data, figsize=(14, 6), show_nodes=False, show_bc=False, material_table=False, label_elements=False, label_nodes=False, alpha=0.4):
|
|
8
|
+
"""
|
|
9
|
+
Plots a mesh colored by material zone.
|
|
10
|
+
Supports both triangular and quadrilateral elements.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
seep_data: Dictionary containing seepage data from import_seep2d
|
|
14
|
+
show_nodes: If True, plot node points
|
|
15
|
+
show_bc: If True, plot boundary condition nodes
|
|
16
|
+
material_table: If True, show material table
|
|
17
|
+
label_elements: If True, label each element with its number at its centroid
|
|
18
|
+
label_nodes: If True, label each node with its number just above and to the right
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from matplotlib.patches import Polygon
|
|
22
|
+
|
|
23
|
+
# Extract data from seep_data
|
|
24
|
+
nodes = seep_data["nodes"]
|
|
25
|
+
elements = seep_data["elements"]
|
|
26
|
+
element_materials = seep_data["element_materials"]
|
|
27
|
+
element_types = seep_data.get("element_types", None) # New field for element types
|
|
28
|
+
bc_type = seep_data["bc_type"]
|
|
29
|
+
|
|
30
|
+
fig, ax = plt.subplots(figsize=figsize)
|
|
31
|
+
materials = np.unique(element_materials)
|
|
32
|
+
|
|
33
|
+
# Import get_material_color to ensure consistent colors with plot_mesh
|
|
34
|
+
from .plot import get_material_color
|
|
35
|
+
mat_to_color = {mat: get_material_color(mat) for mat in materials}
|
|
36
|
+
|
|
37
|
+
# If element_types is not provided, assume all triangles (backward compatibility)
|
|
38
|
+
if element_types is None:
|
|
39
|
+
element_types = np.full(len(elements), 3)
|
|
40
|
+
|
|
41
|
+
for idx, element_nodes in enumerate(elements):
|
|
42
|
+
element_type = element_types[idx]
|
|
43
|
+
color = mat_to_color[element_materials[idx]]
|
|
44
|
+
|
|
45
|
+
if element_type == 3: # Linear triangle
|
|
46
|
+
polygon_coords = nodes[element_nodes[:3]]
|
|
47
|
+
polygon = Polygon(polygon_coords, edgecolor='k', facecolor=color, linewidth=0.5, alpha=alpha)
|
|
48
|
+
ax.add_patch(polygon)
|
|
49
|
+
|
|
50
|
+
elif element_type == 6: # Quadratic triangle - subdivide into 4 sub-triangles
|
|
51
|
+
# Corner nodes
|
|
52
|
+
n0, n1, n2 = nodes[element_nodes[0]], nodes[element_nodes[1]], nodes[element_nodes[2]]
|
|
53
|
+
# Midpoint nodes - standard GMSH pattern: n3=edge 0-1, n4=edge 1-2, n5=edge 2-0
|
|
54
|
+
n3, n4, n5 = nodes[element_nodes[3]], nodes[element_nodes[4]], nodes[element_nodes[5]]
|
|
55
|
+
|
|
56
|
+
# Create 4 sub-triangles with standard GMSH connectivity
|
|
57
|
+
sub_triangles = [
|
|
58
|
+
[n0, n3, n5], # Corner triangle at node 0 (uses midpoints 0-1 and 2-0)
|
|
59
|
+
[n3, n1, n4], # Corner triangle at node 1 (uses midpoints 0-1 and 1-2)
|
|
60
|
+
[n5, n4, n2], # Corner triangle at node 2 (uses midpoints 2-0 and 1-2)
|
|
61
|
+
[n3, n4, n5] # Center triangle (connects all midpoints)
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
# Add all sub-triangles without internal edges
|
|
65
|
+
for sub_tri in sub_triangles:
|
|
66
|
+
polygon = Polygon(sub_tri, edgecolor='none', facecolor=color, alpha=alpha)
|
|
67
|
+
ax.add_patch(polygon)
|
|
68
|
+
|
|
69
|
+
# Add outer boundary of the tri6 element
|
|
70
|
+
outer_boundary = [n0, n1, n2, n0] # Close the triangle
|
|
71
|
+
ax.plot([p[0] for p in outer_boundary], [p[1] for p in outer_boundary],
|
|
72
|
+
'k-', linewidth=0.5)
|
|
73
|
+
|
|
74
|
+
elif element_type == 4: # Linear quadrilateral
|
|
75
|
+
polygon_coords = nodes[element_nodes[:4]]
|
|
76
|
+
polygon = Polygon(polygon_coords, edgecolor='k', facecolor=color, linewidth=0.5, alpha=alpha)
|
|
77
|
+
ax.add_patch(polygon)
|
|
78
|
+
|
|
79
|
+
elif element_type == 8: # Quadratic quadrilateral - subdivide into 4 sub-quads
|
|
80
|
+
# Corner nodes
|
|
81
|
+
n0, n1, n2, n3 = nodes[element_nodes[0]], nodes[element_nodes[1]], nodes[element_nodes[2]], nodes[element_nodes[3]]
|
|
82
|
+
# Midpoint nodes
|
|
83
|
+
n4, n5, n6, n7 = nodes[element_nodes[4]], nodes[element_nodes[5]], nodes[element_nodes[6]], nodes[element_nodes[7]]
|
|
84
|
+
|
|
85
|
+
# Calculate center point (average of all 8 nodes)
|
|
86
|
+
center = ((n0[0] + n1[0] + n2[0] + n3[0] + n4[0] + n5[0] + n6[0] + n7[0]) / 8,
|
|
87
|
+
(n0[1] + n1[1] + n2[1] + n3[1] + n4[1] + n5[1] + n6[1] + n7[1]) / 8)
|
|
88
|
+
|
|
89
|
+
# Create 4 sub-quadrilaterals
|
|
90
|
+
sub_quads = [
|
|
91
|
+
[n0, n4, center, n7], # Sub-quad at corner 0
|
|
92
|
+
[n4, n1, n5, center], # Sub-quad at corner 1
|
|
93
|
+
[center, n5, n2, n6], # Sub-quad at corner 2
|
|
94
|
+
[n7, center, n6, n3] # Sub-quad at corner 3
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
# Add all sub-quads without internal edges
|
|
98
|
+
for sub_quad in sub_quads:
|
|
99
|
+
polygon = Polygon(sub_quad, edgecolor='none', facecolor=color, alpha=alpha)
|
|
100
|
+
ax.add_patch(polygon)
|
|
101
|
+
|
|
102
|
+
# Add outer boundary of the quad8 element
|
|
103
|
+
outer_boundary = [n0, n1, n2, n3, n0] # Close the quadrilateral
|
|
104
|
+
ax.plot([p[0] for p in outer_boundary], [p[1] for p in outer_boundary],
|
|
105
|
+
'k-', linewidth=0.5)
|
|
106
|
+
|
|
107
|
+
elif element_type == 9: # 9-node quadrilateral - subdivide using actual center node
|
|
108
|
+
# Corner nodes
|
|
109
|
+
n0, n1, n2, n3 = nodes[element_nodes[0]], nodes[element_nodes[1]], nodes[element_nodes[2]], nodes[element_nodes[3]]
|
|
110
|
+
# Midpoint nodes
|
|
111
|
+
n4, n5, n6, n7 = nodes[element_nodes[4]], nodes[element_nodes[5]], nodes[element_nodes[6]], nodes[element_nodes[7]]
|
|
112
|
+
# Center node
|
|
113
|
+
center = nodes[element_nodes[8]]
|
|
114
|
+
|
|
115
|
+
# Create 4 sub-quadrilaterals using the actual center node
|
|
116
|
+
sub_quads = [
|
|
117
|
+
[n0, n4, center, n7], # Sub-quad at corner 0
|
|
118
|
+
[n4, n1, n5, center], # Sub-quad at corner 1
|
|
119
|
+
[center, n5, n2, n6], # Sub-quad at corner 2
|
|
120
|
+
[n7, center, n6, n3] # Sub-quad at corner 3
|
|
121
|
+
]
|
|
122
|
+
|
|
123
|
+
# Add all sub-quads without internal edges
|
|
124
|
+
for sub_quad in sub_quads:
|
|
125
|
+
polygon = Polygon(sub_quad, edgecolor='none', facecolor=color, alpha=alpha)
|
|
126
|
+
ax.add_patch(polygon)
|
|
127
|
+
|
|
128
|
+
# Add outer boundary of the quad9 element
|
|
129
|
+
outer_boundary = [n0, n1, n2, n3, n0] # Close the quadrilateral
|
|
130
|
+
ax.plot([p[0] for p in outer_boundary], [p[1] for p in outer_boundary],
|
|
131
|
+
'k-', linewidth=0.5)
|
|
132
|
+
|
|
133
|
+
# Label element number at centroid if requested
|
|
134
|
+
if label_elements:
|
|
135
|
+
# Calculate centroid based on element type
|
|
136
|
+
if element_type in [3, 4]:
|
|
137
|
+
# For linear elements, use the polygon_coords
|
|
138
|
+
if element_type == 3:
|
|
139
|
+
element_coords = nodes[element_nodes[:3]]
|
|
140
|
+
else:
|
|
141
|
+
element_coords = nodes[element_nodes[:4]]
|
|
142
|
+
else:
|
|
143
|
+
# For quadratic elements, use all nodes to calculate centroid
|
|
144
|
+
if element_type == 6:
|
|
145
|
+
element_coords = nodes[element_nodes[:6]]
|
|
146
|
+
elif element_type == 8:
|
|
147
|
+
element_coords = nodes[element_nodes[:8]]
|
|
148
|
+
else: # element_type == 9
|
|
149
|
+
element_coords = nodes[element_nodes[:9]]
|
|
150
|
+
|
|
151
|
+
centroid = np.mean(element_coords, axis=0)
|
|
152
|
+
ax.text(centroid[0], centroid[1], str(idx+1),
|
|
153
|
+
ha='center', va='center', fontsize=6, color='black', alpha=0.4,
|
|
154
|
+
zorder=10)
|
|
155
|
+
|
|
156
|
+
if show_nodes:
|
|
157
|
+
ax.plot(nodes[:, 0], nodes[:, 1], 'k.', markersize=2)
|
|
158
|
+
|
|
159
|
+
# Label node numbers if requested
|
|
160
|
+
if label_nodes:
|
|
161
|
+
for i, (x, y) in enumerate(nodes):
|
|
162
|
+
ax.text(x + 0.5, y + 0.5, str(i+1), fontsize=6, color='blue', alpha=0.7,
|
|
163
|
+
ha='left', va='bottom', zorder=11)
|
|
164
|
+
|
|
165
|
+
# Get material names if available
|
|
166
|
+
material_names = seep_data.get("material_names", [])
|
|
167
|
+
|
|
168
|
+
legend_handles = []
|
|
169
|
+
for mat in materials:
|
|
170
|
+
# Use material name if available, otherwise use "Material {mat}"
|
|
171
|
+
if material_names and mat <= len(material_names):
|
|
172
|
+
label = material_names[mat - 1] # Convert to 0-based index
|
|
173
|
+
else:
|
|
174
|
+
label = f"Material {mat}"
|
|
175
|
+
|
|
176
|
+
legend_handles.append(
|
|
177
|
+
plt.Line2D([0], [0], color=mat_to_color[mat], lw=4, label=label)
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
if show_bc:
|
|
181
|
+
bc1 = nodes[bc_type == 1]
|
|
182
|
+
bc2 = nodes[bc_type == 2]
|
|
183
|
+
if len(bc1) > 0:
|
|
184
|
+
h1, = ax.plot(bc1[:, 0], bc1[:, 1], 'ro', label="Fixed Head (bc_type=1)")
|
|
185
|
+
legend_handles.append(h1)
|
|
186
|
+
if len(bc2) > 0:
|
|
187
|
+
h2, = ax.plot(bc2[:, 0], bc2[:, 1], 'bs', label="Exit Face (bc_type=2)")
|
|
188
|
+
legend_handles.append(h2)
|
|
189
|
+
|
|
190
|
+
# Single combined legend outside the plot
|
|
191
|
+
ax.legend(
|
|
192
|
+
handles=legend_handles,
|
|
193
|
+
loc='upper center',
|
|
194
|
+
bbox_to_anchor=(0.5, -0.1),
|
|
195
|
+
ncol=3, # or more, depending on how many items you have
|
|
196
|
+
frameon=False
|
|
197
|
+
)
|
|
198
|
+
ax.set_aspect("equal")
|
|
199
|
+
|
|
200
|
+
# Count element types for title
|
|
201
|
+
num_triangles = np.sum(element_types == 3)
|
|
202
|
+
num_quads = np.sum(element_types == 4)
|
|
203
|
+
if num_triangles > 0 and num_quads > 0:
|
|
204
|
+
title = f"SEEP2D Mesh with Material Zones ({num_triangles} triangles, {num_quads} quads)"
|
|
205
|
+
elif num_quads > 0:
|
|
206
|
+
title = f"SEEP2D Mesh with Material Zones ({num_quads} quadrilaterals)"
|
|
207
|
+
else:
|
|
208
|
+
title = f"SEEP2D Mesh with Material Zones ({num_triangles} triangles)"
|
|
209
|
+
|
|
210
|
+
# Place the table in the upper left
|
|
211
|
+
if material_table:
|
|
212
|
+
plot_seep_material_table(ax, seep_data, xloc=0.3, yloc=1.1) # upper left
|
|
213
|
+
|
|
214
|
+
ax.set_title(title)
|
|
215
|
+
# plt.subplots_adjust(bottom=0.2) # Add vertical cushion
|
|
216
|
+
plt.tight_layout()
|
|
217
|
+
plt.show()
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
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, show_mesh=True):
|
|
221
|
+
"""
|
|
222
|
+
Plots head contours and optionally overlays flowlines (phi) based on flow function.
|
|
223
|
+
Fixed version that properly handles mesh aspect ratio and doesn't clip the plot.
|
|
224
|
+
Supports both triangular and quadrilateral elements.
|
|
225
|
+
|
|
226
|
+
Arguments:
|
|
227
|
+
seep_data: Dictionary containing seepage data from import_seep2d
|
|
228
|
+
solution: Dictionary containing solution results from run_analysis
|
|
229
|
+
levels: number of head contour levels
|
|
230
|
+
base_mat: material ID (1-based) used to compute k for flow function
|
|
231
|
+
fill_contours: bool, if True shows filled contours, if False only black solid lines
|
|
232
|
+
phreatic: bool, if True plots phreatic surface (pressure head = 0) as thick red line
|
|
233
|
+
show_mesh: bool, if True overlays element edges in light gray
|
|
234
|
+
"""
|
|
235
|
+
import matplotlib.pyplot as plt
|
|
236
|
+
import matplotlib.tri as tri
|
|
237
|
+
from matplotlib.ticker import MaxNLocator
|
|
238
|
+
from matplotlib.patches import Polygon
|
|
239
|
+
import numpy as np
|
|
240
|
+
|
|
241
|
+
# Extract data from seep_data and solution
|
|
242
|
+
nodes = seep_data["nodes"]
|
|
243
|
+
elements = seep_data["elements"]
|
|
244
|
+
element_materials = seep_data["element_materials"]
|
|
245
|
+
element_types = seep_data.get("element_types", None) # New field for element types
|
|
246
|
+
k1_by_mat = seep_data.get("k1_by_mat") # Use .get() in case it's not present
|
|
247
|
+
head = solution["head"]
|
|
248
|
+
phi = solution.get("phi")
|
|
249
|
+
flowrate = solution.get("flowrate")
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
# Use constrained_layout for best layout
|
|
253
|
+
fig, ax = plt.subplots(figsize=figsize, constrained_layout=True)
|
|
254
|
+
|
|
255
|
+
# If element_types is not provided, assume all triangles (backward compatibility)
|
|
256
|
+
if element_types is None:
|
|
257
|
+
element_types = np.full(len(elements), 3)
|
|
258
|
+
|
|
259
|
+
# Count element types
|
|
260
|
+
tri3_count = np.sum(element_types == 3)
|
|
261
|
+
tri6_count = np.sum(element_types == 6)
|
|
262
|
+
quad4_count = np.sum(element_types == 4)
|
|
263
|
+
quad8_count = np.sum(element_types == 8)
|
|
264
|
+
quad9_count = np.sum(element_types == 9)
|
|
265
|
+
|
|
266
|
+
print(f"Plotting {tri3_count} linear triangles, {tri6_count} quadratic triangles, "
|
|
267
|
+
f"{quad4_count} linear quads, {quad8_count} 8-node quads, {quad9_count} 9-node quads")
|
|
268
|
+
|
|
269
|
+
# Plot material zones first (if element_materials provided)
|
|
270
|
+
if element_materials is not None:
|
|
271
|
+
materials = np.unique(element_materials)
|
|
272
|
+
|
|
273
|
+
# Import get_material_color to ensure consistent colors with plot_mesh
|
|
274
|
+
from .plot import get_material_color
|
|
275
|
+
mat_to_color = {mat: get_material_color(mat) for mat in materials}
|
|
276
|
+
|
|
277
|
+
# Plot all elements with proper subdivision for quadratic elements
|
|
278
|
+
for idx, element_nodes in enumerate(elements):
|
|
279
|
+
element_type = element_types[idx]
|
|
280
|
+
color = mat_to_color[element_materials[idx]]
|
|
281
|
+
|
|
282
|
+
if element_type == 3: # Linear triangle
|
|
283
|
+
polygon = nodes[element_nodes[:3]]
|
|
284
|
+
ax.fill(*zip(*polygon), edgecolor='none', facecolor=color, alpha=alpha)
|
|
285
|
+
|
|
286
|
+
elif element_type == 6: # Quadratic triangle - subdivide into 4 sub-triangles
|
|
287
|
+
# Corner nodes
|
|
288
|
+
n0, n1, n2 = nodes[element_nodes[0]], nodes[element_nodes[1]], nodes[element_nodes[2]]
|
|
289
|
+
# Midpoint nodes - standard GMSH pattern: n3=edge 0-1, n4=edge 1-2, n5=edge 2-0
|
|
290
|
+
n3, n4, n5 = nodes[element_nodes[3]], nodes[element_nodes[4]], nodes[element_nodes[5]]
|
|
291
|
+
|
|
292
|
+
# Create 4 sub-triangles with standard GMSH connectivity
|
|
293
|
+
sub_triangles = [
|
|
294
|
+
[n0, n3, n5], # Corner triangle at node 0 (uses midpoints 0-1 and 2-0)
|
|
295
|
+
[n3, n1, n4], # Corner triangle at node 1 (uses midpoints 0-1 and 1-2)
|
|
296
|
+
[n5, n4, n2], # Corner triangle at node 2 (uses midpoints 2-0 and 1-2)
|
|
297
|
+
[n3, n4, n5] # Center triangle (connects all midpoints)
|
|
298
|
+
]
|
|
299
|
+
|
|
300
|
+
# Plot all sub-triangles
|
|
301
|
+
for sub_tri in sub_triangles:
|
|
302
|
+
ax.fill(*zip(*sub_tri), edgecolor='none', facecolor=color, alpha=alpha)
|
|
303
|
+
|
|
304
|
+
elif element_type == 4: # Linear quadrilateral
|
|
305
|
+
polygon = nodes[element_nodes[:4]]
|
|
306
|
+
ax.fill(*zip(*polygon), edgecolor='none', facecolor=color, alpha=alpha)
|
|
307
|
+
|
|
308
|
+
elif element_type == 8: # Quadratic quadrilateral - subdivide into 4 sub-quads
|
|
309
|
+
# Corner nodes
|
|
310
|
+
n0, n1, n2, n3 = nodes[element_nodes[0]], nodes[element_nodes[1]], nodes[element_nodes[2]], nodes[element_nodes[3]]
|
|
311
|
+
# Midpoint nodes
|
|
312
|
+
n4, n5, n6, n7 = nodes[element_nodes[4]], nodes[element_nodes[5]], nodes[element_nodes[6]], nodes[element_nodes[7]]
|
|
313
|
+
|
|
314
|
+
# Calculate center point (average of all 8 nodes)
|
|
315
|
+
center = ((n0[0] + n1[0] + n2[0] + n3[0] + n4[0] + n5[0] + n6[0] + n7[0]) / 8,
|
|
316
|
+
(n0[1] + n1[1] + n2[1] + n3[1] + n4[1] + n5[1] + n6[1] + n7[1]) / 8)
|
|
317
|
+
|
|
318
|
+
# Create 4 sub-quadrilaterals
|
|
319
|
+
sub_quads = [
|
|
320
|
+
[n0, n4, center, n7], # Sub-quad at corner 0
|
|
321
|
+
[n4, n1, n5, center], # Sub-quad at corner 1
|
|
322
|
+
[center, n5, n2, n6], # Sub-quad at corner 2
|
|
323
|
+
[n7, center, n6, n3] # Sub-quad at corner 3
|
|
324
|
+
]
|
|
325
|
+
|
|
326
|
+
# Plot all sub-quads
|
|
327
|
+
for sub_quad in sub_quads:
|
|
328
|
+
ax.fill(*zip(*sub_quad), edgecolor='none', facecolor=color, alpha=alpha)
|
|
329
|
+
|
|
330
|
+
elif element_type == 9: # 9-node quadrilateral - subdivide using actual center node
|
|
331
|
+
# Corner nodes
|
|
332
|
+
n0, n1, n2, n3 = nodes[element_nodes[0]], nodes[element_nodes[1]], nodes[element_nodes[2]], nodes[element_nodes[3]]
|
|
333
|
+
# Midpoint nodes
|
|
334
|
+
n4, n5, n6, n7 = nodes[element_nodes[4]], nodes[element_nodes[5]], nodes[element_nodes[6]], nodes[element_nodes[7]]
|
|
335
|
+
# Center node
|
|
336
|
+
center = nodes[element_nodes[8]]
|
|
337
|
+
|
|
338
|
+
# Create 4 sub-quadrilaterals using the actual center node
|
|
339
|
+
sub_quads = [
|
|
340
|
+
[n0, n4, center, n7], # Sub-quad at corner 0
|
|
341
|
+
[n4, n1, n5, center], # Sub-quad at corner 1
|
|
342
|
+
[center, n5, n2, n6], # Sub-quad at corner 2
|
|
343
|
+
[n7, center, n6, n3] # Sub-quad at corner 3
|
|
344
|
+
]
|
|
345
|
+
|
|
346
|
+
# Plot all sub-quads
|
|
347
|
+
for sub_quad in sub_quads:
|
|
348
|
+
ax.fill(*zip(*sub_quad), edgecolor='none', facecolor=color, alpha=alpha)
|
|
349
|
+
|
|
350
|
+
vmin = np.min(head)
|
|
351
|
+
vmax = np.max(head)
|
|
352
|
+
hdrop = vmax - vmin
|
|
353
|
+
contour_levels = np.linspace(vmin, vmax, levels)
|
|
354
|
+
|
|
355
|
+
# For contouring, subdivide tri6 elements into 4 subtriangles
|
|
356
|
+
all_triangles_for_contouring = []
|
|
357
|
+
for idx, element_nodes in enumerate(elements):
|
|
358
|
+
element_type = element_types[idx]
|
|
359
|
+
if element_type == 3: # Linear triangular elements
|
|
360
|
+
all_triangles_for_contouring.append(element_nodes[:3])
|
|
361
|
+
elif element_type == 6: # Quadratic triangular elements
|
|
362
|
+
# Standard GMSH tri6 ordering: 3 = edge 0-1; 4 = edge 1-2; 5 = edge 2-0
|
|
363
|
+
# Create 4 subtriangles: 0-3-5, 3-1-4, 5-4-2, 3-4-5
|
|
364
|
+
subtriangles = [
|
|
365
|
+
[element_nodes[0], element_nodes[3], element_nodes[5]], # 0-3-5 (corner at 0)
|
|
366
|
+
[element_nodes[3], element_nodes[1], element_nodes[4]], # 3-1-4 (corner at 1)
|
|
367
|
+
[element_nodes[5], element_nodes[4], element_nodes[2]], # 5-4-2 (corner at 2)
|
|
368
|
+
[element_nodes[3], element_nodes[4], element_nodes[5]] # 3-4-5 (center)
|
|
369
|
+
]
|
|
370
|
+
all_triangles_for_contouring.extend(subtriangles)
|
|
371
|
+
elif element_type in [4, 8, 9]: # Quadrilateral elements
|
|
372
|
+
tri1 = [element_nodes[0], element_nodes[1], element_nodes[2]]
|
|
373
|
+
tri2 = [element_nodes[0], element_nodes[2], element_nodes[3]]
|
|
374
|
+
all_triangles_for_contouring.extend([tri1, tri2])
|
|
375
|
+
triang = tri.Triangulation(nodes[:, 0], nodes[:, 1], all_triangles_for_contouring)
|
|
376
|
+
|
|
377
|
+
# Filled contours (only if fill_contours=True)
|
|
378
|
+
if fill_contours:
|
|
379
|
+
contourf = ax.tricontourf(triang, head, levels=contour_levels, cmap="Spectral_r", vmin=vmin, vmax=vmax, alpha=0.5)
|
|
380
|
+
cbar = plt.colorbar(contourf, ax=ax, label="Total Head", shrink=0.8, pad=0.02)
|
|
381
|
+
cbar.locator = MaxNLocator(nbins=10, steps=[1, 2, 5])
|
|
382
|
+
cbar.update_ticks()
|
|
383
|
+
|
|
384
|
+
# Solid lines for head contours
|
|
385
|
+
ax.tricontour(triang, head, levels=contour_levels, colors="k", linewidths=0.5)
|
|
386
|
+
|
|
387
|
+
# Phreatic surface (pressure head = 0)
|
|
388
|
+
if phreatic:
|
|
389
|
+
elevation = nodes[:, 1] # y-coordinate is elevation
|
|
390
|
+
pressure_head = head - elevation
|
|
391
|
+
ax.tricontour(triang, pressure_head, levels=[0], colors="red", linewidths=2.0)
|
|
392
|
+
|
|
393
|
+
# Overlay flowlines if phi is available
|
|
394
|
+
if phi is not None and flowrate is not None and k1_by_mat is not None:
|
|
395
|
+
if base_mat > len(k1_by_mat):
|
|
396
|
+
print(f"Warning: base_mat={base_mat} is larger than number of materials ({len(k1_by_mat)}). Using material 1.")
|
|
397
|
+
base_mat = 1
|
|
398
|
+
elif base_mat < 1:
|
|
399
|
+
print(f"Warning: base_mat={base_mat} is less than 1. Using material 1.")
|
|
400
|
+
base_mat = 1
|
|
401
|
+
base_k = k1_by_mat[base_mat - 1]
|
|
402
|
+
ne = levels - 1
|
|
403
|
+
nf = (flowrate * ne) / (base_k * hdrop)
|
|
404
|
+
phi_levels = round(nf) + 1
|
|
405
|
+
print(f"Computed nf: {nf:.2f}, using {phi_levels} φ contours (flowrate={flowrate:.3f}, base k={base_k}, head drop={hdrop:.3f})")
|
|
406
|
+
phi_contours = np.linspace(np.min(phi), np.max(phi), phi_levels)
|
|
407
|
+
ax.tricontour(triang, phi, levels=phi_contours, colors="blue", linewidths=0.7, linestyles="solid")
|
|
408
|
+
|
|
409
|
+
# Plot element edges if requested
|
|
410
|
+
if show_mesh:
|
|
411
|
+
# Draw all element edges
|
|
412
|
+
for element, elem_type in zip(elements, element_types if element_types is not None else [3]*len(elements)):
|
|
413
|
+
if elem_type == 3:
|
|
414
|
+
# Triangle: connect nodes 0-1-2-0
|
|
415
|
+
edge_nodes = [element[0], element[1], element[2], element[0]]
|
|
416
|
+
elif elem_type == 4:
|
|
417
|
+
# Quadrilateral: connect nodes 0-1-2-3-0
|
|
418
|
+
edge_nodes = [element[0], element[1], element[2], element[3], element[0]]
|
|
419
|
+
elif elem_type == 6:
|
|
420
|
+
# 6-node triangle: only connect corner nodes 0-1-2-0
|
|
421
|
+
edge_nodes = [element[0], element[1], element[2], element[0]]
|
|
422
|
+
elif elem_type in [8, 9]:
|
|
423
|
+
# Higher-order quads: only connect corner nodes 0-1-2-3-0
|
|
424
|
+
edge_nodes = [element[0], element[1], element[2], element[3], element[0]]
|
|
425
|
+
else:
|
|
426
|
+
continue # Skip unknown element types
|
|
427
|
+
|
|
428
|
+
# Get coordinates of edge nodes
|
|
429
|
+
edge_coords = nodes[edge_nodes]
|
|
430
|
+
ax.plot(edge_coords[:, 0], edge_coords[:, 1], color="darkgray", linewidth=0.5, alpha=0.7)
|
|
431
|
+
|
|
432
|
+
# Plot the mesh boundary
|
|
433
|
+
try:
|
|
434
|
+
boundary = get_ordered_mesh_boundary(nodes, elements, element_types)
|
|
435
|
+
ax.plot(boundary[:, 0], boundary[:, 1], color="black", linewidth=1.0, label="Mesh Boundary")
|
|
436
|
+
except Exception as e:
|
|
437
|
+
print(f"Warning: Could not plot mesh boundary: {e}")
|
|
438
|
+
|
|
439
|
+
# Add cushion around the mesh
|
|
440
|
+
x_min, x_max = nodes[:, 0].min(), nodes[:, 0].max()
|
|
441
|
+
y_min, y_max = nodes[:, 1].min(), nodes[:, 1].max()
|
|
442
|
+
x_pad = (x_max - x_min) * pad_frac
|
|
443
|
+
y_pad = (y_max - y_min) * pad_frac
|
|
444
|
+
ax.set_xlim(x_min - x_pad, x_max + x_pad)
|
|
445
|
+
ax.set_ylim(y_min - y_pad, y_max + y_pad)
|
|
446
|
+
|
|
447
|
+
title = "Flow Net: Head Contours"
|
|
448
|
+
if phi is not None:
|
|
449
|
+
title += " and Flowlines"
|
|
450
|
+
if phreatic:
|
|
451
|
+
title += " with Phreatic Surface"
|
|
452
|
+
if flowrate is not None:
|
|
453
|
+
title += f" — Total Flowrate: {flowrate:.3f}"
|
|
454
|
+
ax.set_title(title)
|
|
455
|
+
|
|
456
|
+
# Set equal aspect ratio AFTER setting limits
|
|
457
|
+
ax.set_aspect("equal")
|
|
458
|
+
|
|
459
|
+
# Remove tight_layout and subplots_adjust for best constrained layout
|
|
460
|
+
# plt.tight_layout()
|
|
461
|
+
# plt.subplots_adjust(top=0.78)
|
|
462
|
+
plt.show()
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def plot_seep_material_table(ax, seep_data, xloc=0.6, yloc=0.7):
|
|
466
|
+
"""
|
|
467
|
+
Adds a seepage material properties table to the plot.
|
|
468
|
+
|
|
469
|
+
Parameters:
|
|
470
|
+
ax: matplotlib Axes object
|
|
471
|
+
seep_data: Dictionary containing seepage data with material properties
|
|
472
|
+
xloc: x-location of table (0-1)
|
|
473
|
+
yloc: y-location of table (0-1)
|
|
474
|
+
|
|
475
|
+
Returns:
|
|
476
|
+
None
|
|
477
|
+
"""
|
|
478
|
+
# Extract material properties from seep_data
|
|
479
|
+
k1_by_mat = seep_data.get("k1_by_mat")
|
|
480
|
+
k2_by_mat = seep_data.get("k2_by_mat")
|
|
481
|
+
angle_by_mat = seep_data.get("angle_by_mat")
|
|
482
|
+
kr0_by_mat = seep_data.get("kr0_by_mat")
|
|
483
|
+
h0_by_mat = seep_data.get("h0_by_mat")
|
|
484
|
+
material_names = seep_data.get("material_names", [])
|
|
485
|
+
|
|
486
|
+
if k1_by_mat is None or len(k1_by_mat) == 0:
|
|
487
|
+
return
|
|
488
|
+
|
|
489
|
+
# Column headers for seepage properties
|
|
490
|
+
col_labels = ["Mat", "Name", "k₁", "k₂", "Angle", "kr₀", "h₀"]
|
|
491
|
+
|
|
492
|
+
# Build table rows
|
|
493
|
+
table_data = []
|
|
494
|
+
for idx in range(len(k1_by_mat)):
|
|
495
|
+
k1 = k1_by_mat[idx]
|
|
496
|
+
k2 = k2_by_mat[idx] if k2_by_mat is not None else 0.0
|
|
497
|
+
angle = angle_by_mat[idx] if angle_by_mat is not None else 0.0
|
|
498
|
+
kr0 = kr0_by_mat[idx] if kr0_by_mat is not None else 0.0
|
|
499
|
+
h0 = h0_by_mat[idx] if h0_by_mat is not None else 0.0
|
|
500
|
+
|
|
501
|
+
# Get material name, use default if not available
|
|
502
|
+
material_name = material_names[idx] if idx < len(material_names) else f"Material {idx+1}"
|
|
503
|
+
|
|
504
|
+
# Format values with appropriate precision
|
|
505
|
+
row = [
|
|
506
|
+
idx + 1, # Material number (1-based)
|
|
507
|
+
material_name, # Material name
|
|
508
|
+
f"{k1:.3f}", # k1 in scientific notation
|
|
509
|
+
f"{k2:.3f}", # k2 in scientific notation
|
|
510
|
+
f"{angle:.1f}", # angle in degrees
|
|
511
|
+
f"{kr0:.4f}", # kr0
|
|
512
|
+
f"{h0:.2f}" # h0
|
|
513
|
+
]
|
|
514
|
+
table_data.append(row)
|
|
515
|
+
|
|
516
|
+
# Add the table
|
|
517
|
+
table = ax.table(cellText=table_data,
|
|
518
|
+
colLabels=col_labels,
|
|
519
|
+
loc='upper right',
|
|
520
|
+
colLoc='center',
|
|
521
|
+
cellLoc='center',
|
|
522
|
+
bbox=[xloc, yloc, 0.45, 0.25]) # Increased width to accommodate name column
|
|
523
|
+
table.auto_set_font_size(False)
|
|
524
|
+
table.set_fontsize(8)
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def get_ordered_mesh_boundary(nodes, elements, element_types=None):
|
|
528
|
+
"""
|
|
529
|
+
Extracts the outer boundary of the mesh and returns it as an ordered array of points.
|
|
530
|
+
Supports both triangular and quadrilateral elements.
|
|
531
|
+
|
|
532
|
+
Returns:
|
|
533
|
+
np.ndarray of shape (N, 2): boundary coordinates in order (closed loop)
|
|
534
|
+
"""
|
|
535
|
+
import numpy as np
|
|
536
|
+
from collections import defaultdict, deque
|
|
537
|
+
|
|
538
|
+
# If element_types is not provided, assume all triangles (backward compatibility)
|
|
539
|
+
if element_types is None:
|
|
540
|
+
element_types = np.full(len(elements), 3)
|
|
541
|
+
|
|
542
|
+
# Step 1: Count all edges
|
|
543
|
+
edge_count = defaultdict(int)
|
|
544
|
+
edge_to_nodes = {}
|
|
545
|
+
|
|
546
|
+
for i, element_nodes in enumerate(elements):
|
|
547
|
+
element_type = element_types[i]
|
|
548
|
+
|
|
549
|
+
if element_type == 3:
|
|
550
|
+
# Triangle: 3 edges
|
|
551
|
+
for j in range(3):
|
|
552
|
+
a, b = sorted((element_nodes[j], element_nodes[(j + 1) % 3]))
|
|
553
|
+
edge_count[(a, b)] += 1
|
|
554
|
+
edge_to_nodes[(a, b)] = (element_nodes[j], element_nodes[(j + 1) % 3]) # preserve direction
|
|
555
|
+
elif element_type == 4:
|
|
556
|
+
# Quadrilateral: 4 edges
|
|
557
|
+
for j in range(4):
|
|
558
|
+
a, b = sorted((element_nodes[j], element_nodes[(j + 1) % 4]))
|
|
559
|
+
edge_count[(a, b)] += 1
|
|
560
|
+
edge_to_nodes[(a, b)] = (element_nodes[j], element_nodes[(j + 1) % 4]) # preserve direction
|
|
561
|
+
elif element_type == 6:
|
|
562
|
+
# 6-node triangle: 3 edges (use only corner nodes 0,1,2)
|
|
563
|
+
for j in range(3):
|
|
564
|
+
a, b = sorted((element_nodes[j], element_nodes[(j + 1) % 3]))
|
|
565
|
+
edge_count[(a, b)] += 1
|
|
566
|
+
edge_to_nodes[(a, b)] = (element_nodes[j], element_nodes[(j + 1) % 3]) # preserve direction
|
|
567
|
+
elif element_type in [8, 9]:
|
|
568
|
+
# Higher-order quadrilaterals: 4 edges (use only corner nodes 0,1,2,3)
|
|
569
|
+
for j in range(4):
|
|
570
|
+
a, b = sorted((element_nodes[j], element_nodes[(j + 1) % 4]))
|
|
571
|
+
edge_count[(a, b)] += 1
|
|
572
|
+
edge_to_nodes[(a, b)] = (element_nodes[j], element_nodes[(j + 1) % 4]) # preserve direction
|
|
573
|
+
|
|
574
|
+
# Step 2: Keep only boundary edges (appear once)
|
|
575
|
+
boundary_edges = [edge_to_nodes[e] for e, count in edge_count.items() if count == 1]
|
|
576
|
+
|
|
577
|
+
if not boundary_edges:
|
|
578
|
+
raise ValueError("No boundary edges found.")
|
|
579
|
+
|
|
580
|
+
# Step 3: Build adjacency for boundary walk
|
|
581
|
+
adj = defaultdict(list)
|
|
582
|
+
for a, b in boundary_edges:
|
|
583
|
+
adj[a].append(b)
|
|
584
|
+
adj[b].append(a)
|
|
585
|
+
|
|
586
|
+
# Step 4: Walk all boundary segments
|
|
587
|
+
all_boundary_nodes = []
|
|
588
|
+
remaining_edges = set(boundary_edges)
|
|
589
|
+
|
|
590
|
+
while remaining_edges:
|
|
591
|
+
# Start a new boundary segment
|
|
592
|
+
start_edge = remaining_edges.pop()
|
|
593
|
+
start_node = start_edge[0]
|
|
594
|
+
current_node = start_edge[1]
|
|
595
|
+
|
|
596
|
+
segment = [start_node, current_node]
|
|
597
|
+
remaining_edges.discard((current_node, start_node)) # Remove reverse edge if present
|
|
598
|
+
|
|
599
|
+
# Walk this segment until we can't continue
|
|
600
|
+
while True:
|
|
601
|
+
# Find next edge from current node
|
|
602
|
+
next_edge = None
|
|
603
|
+
for edge in remaining_edges:
|
|
604
|
+
if edge[0] == current_node:
|
|
605
|
+
next_edge = edge
|
|
606
|
+
break
|
|
607
|
+
elif edge[1] == current_node:
|
|
608
|
+
next_edge = (edge[1], edge[0]) # Reverse the edge
|
|
609
|
+
break
|
|
610
|
+
|
|
611
|
+
if next_edge is None:
|
|
612
|
+
break
|
|
613
|
+
|
|
614
|
+
next_node = next_edge[1]
|
|
615
|
+
segment.append(next_node)
|
|
616
|
+
remaining_edges.discard(next_edge)
|
|
617
|
+
remaining_edges.discard((next_node, current_node)) # Remove reverse edge if present
|
|
618
|
+
current_node = next_node
|
|
619
|
+
|
|
620
|
+
# Check if we've closed the loop
|
|
621
|
+
if current_node == start_node:
|
|
622
|
+
break
|
|
623
|
+
|
|
624
|
+
all_boundary_nodes.extend(segment)
|
|
625
|
+
|
|
626
|
+
# If we have multiple segments, we need to handle them properly
|
|
627
|
+
# For now, just return the first complete segment
|
|
628
|
+
if all_boundary_nodes:
|
|
629
|
+
# Ensure the boundary is closed
|
|
630
|
+
if all_boundary_nodes[0] != all_boundary_nodes[-1]:
|
|
631
|
+
all_boundary_nodes.append(all_boundary_nodes[0])
|
|
632
|
+
return nodes[all_boundary_nodes]
|
|
633
|
+
else:
|
|
634
|
+
raise ValueError("No boundary nodes found.")
|