xslope 0.1.6__py3-none-any.whl → 0.1.8__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/mesh copy.py +2962 -0
- xslope/mesh.py +156 -9
- xslope/plot.py +4 -2
- xslope/plot_seep.py +157 -37
- xslope/seep.py +172 -3
- {xslope-0.1.6.dist-info → xslope-0.1.8.dist-info}/METADATA +1 -1
- xslope-0.1.8.dist-info/RECORD +21 -0
- xslope-0.1.6.dist-info/RECORD +0 -20
- {xslope-0.1.6.dist-info → xslope-0.1.8.dist-info}/LICENSE +0 -0
- {xslope-0.1.6.dist-info → xslope-0.1.8.dist-info}/NOTICE +0 -0
- {xslope-0.1.6.dist-info → xslope-0.1.8.dist-info}/WHEEL +0 -0
- {xslope-0.1.6.dist-info → xslope-0.1.8.dist-info}/top_level.txt +0 -0
xslope/mesh.py
CHANGED
|
@@ -1305,6 +1305,12 @@ def build_polygons(slope_data, reinf_lines=None, debug=False):
|
|
|
1305
1305
|
proj_left_x = None
|
|
1306
1306
|
proj_right_x = None
|
|
1307
1307
|
bottom_cleaned = []
|
|
1308
|
+
|
|
1309
|
+
# Initialize vertical edge points (used for intermediate points on vertical edges)
|
|
1310
|
+
left_vertical_points = [] # Intermediate points on left vertical edge (bottom to top)
|
|
1311
|
+
right_vertical_points = [] # Intermediate points on right vertical edge (top to bottom)
|
|
1312
|
+
left_y_bot = -np.inf
|
|
1313
|
+
right_y_bot = -np.inf
|
|
1308
1314
|
|
|
1309
1315
|
if i < n - 1:
|
|
1310
1316
|
# Use the immediate next line as the lower boundary
|
|
@@ -1356,21 +1362,112 @@ def build_polygons(slope_data, reinf_lines=None, debug=False):
|
|
|
1356
1362
|
# Convert to sorted list
|
|
1357
1363
|
bottom_cleaned = sorted([(orig_x, orig_y) for _, _, orig_x, orig_y in bottom_dict.values()])
|
|
1358
1364
|
|
|
1365
|
+
# Helper function to check if a point already exists in a list
|
|
1366
|
+
def point_exists(point_list, x, y, tol=1e-8):
|
|
1367
|
+
"""Check if a point (x, y) already exists in the point list within tolerance."""
|
|
1368
|
+
for px, py in point_list:
|
|
1369
|
+
if abs(px - x) < tol and abs(py - y) < tol:
|
|
1370
|
+
return True
|
|
1371
|
+
return False
|
|
1372
|
+
|
|
1373
|
+
# Helper function to find the lowest y value at a given x by checking all segments
|
|
1374
|
+
def find_lowest_y_at_x(line_points, x_query, tol=1e-8):
|
|
1375
|
+
"""
|
|
1376
|
+
Find the lowest y value at x_query by checking all segments of the line.
|
|
1377
|
+
Handles vertical segments properly by finding all y values at that x and returning the minimum.
|
|
1378
|
+
|
|
1379
|
+
Returns:
|
|
1380
|
+
tuple: (y_value, is_at_endpoint) where is_at_endpoint indicates if x_query is at an endpoint
|
|
1381
|
+
"""
|
|
1382
|
+
if not line_points:
|
|
1383
|
+
return None, False
|
|
1384
|
+
|
|
1385
|
+
xs = np.array([x for x, y in line_points])
|
|
1386
|
+
ys = np.array([y for x, y in line_points])
|
|
1387
|
+
|
|
1388
|
+
# Check if x_query is within the line's x-range
|
|
1389
|
+
if xs[0] - tol > x_query or xs[-1] + tol < x_query:
|
|
1390
|
+
return None, False
|
|
1391
|
+
|
|
1392
|
+
# Check if x_query is at an endpoint
|
|
1393
|
+
is_at_left_endpoint = abs(x_query - xs[0]) < tol
|
|
1394
|
+
is_at_right_endpoint = abs(x_query - xs[-1]) < tol
|
|
1395
|
+
is_at_endpoint = is_at_left_endpoint or is_at_right_endpoint
|
|
1396
|
+
|
|
1397
|
+
# Find all y values at x_query by checking all segments
|
|
1398
|
+
y_values = []
|
|
1399
|
+
|
|
1400
|
+
# Check all points that are exactly at x_query
|
|
1401
|
+
for k in range(len(line_points)):
|
|
1402
|
+
if abs(xs[k] - x_query) < tol:
|
|
1403
|
+
y_values.append(ys[k])
|
|
1404
|
+
|
|
1405
|
+
# Check all segments that contain x_query
|
|
1406
|
+
for k in range(len(line_points) - 1):
|
|
1407
|
+
x1, y1 = line_points[k]
|
|
1408
|
+
x2, y2 = line_points[k + 1]
|
|
1409
|
+
|
|
1410
|
+
# Check if segment is vertical and contains x_query
|
|
1411
|
+
if abs(x1 - x_query) < tol and abs(x2 - x_query) < tol:
|
|
1412
|
+
# Vertical segment - include both y values
|
|
1413
|
+
y_values.append(y1)
|
|
1414
|
+
y_values.append(y2)
|
|
1415
|
+
# Check if segment is horizontal or sloped and contains x_query
|
|
1416
|
+
elif min(x1, x2) - tol <= x_query <= max(x1, x2) + tol:
|
|
1417
|
+
# Interpolate y value
|
|
1418
|
+
if abs(x2 - x1) < tol:
|
|
1419
|
+
# Segment is vertical (should have been caught above, but just in case)
|
|
1420
|
+
y_values.append(y1)
|
|
1421
|
+
y_values.append(y2)
|
|
1422
|
+
else:
|
|
1423
|
+
# Linear interpolation
|
|
1424
|
+
t = (x_query - x1) / (x2 - x1)
|
|
1425
|
+
if 0 <= t <= 1:
|
|
1426
|
+
y_interp = y1 + t * (y2 - y1)
|
|
1427
|
+
y_values.append(y_interp)
|
|
1428
|
+
|
|
1429
|
+
if not y_values:
|
|
1430
|
+
return None, False
|
|
1431
|
+
|
|
1432
|
+
# Return the lowest y value
|
|
1433
|
+
y_min = min(y_values)
|
|
1434
|
+
return y_min, is_at_endpoint
|
|
1435
|
+
|
|
1359
1436
|
# Project endpoints - find highest lower profile or use max_depth
|
|
1360
|
-
|
|
1361
|
-
|
|
1437
|
+
# When projecting right side: if intersection is at left end of lower line,
|
|
1438
|
+
# add that point but continue projecting down
|
|
1439
|
+
# When projecting left side: if intersection is at right end of lower line,
|
|
1440
|
+
# add that point but continue projecting down
|
|
1362
1441
|
for j in range(i + 1, n):
|
|
1363
1442
|
lower_candidate = lines[j]
|
|
1364
1443
|
xs_cand = np.array([x for x, y in lower_candidate])
|
|
1365
1444
|
ys_cand = np.array([y for x, y in lower_candidate])
|
|
1445
|
+
|
|
1446
|
+
# Check left endpoint projection
|
|
1366
1447
|
if xs_cand[0] - tol <= left_x <= xs_cand[-1] + tol:
|
|
1367
|
-
y_cand =
|
|
1368
|
-
if y_cand
|
|
1369
|
-
|
|
1448
|
+
y_cand, is_at_endpoint = find_lowest_y_at_x(lower_candidate, left_x, tol)
|
|
1449
|
+
if y_cand is not None:
|
|
1450
|
+
# If intersection is at the right end of the lower line, add point but continue
|
|
1451
|
+
if is_at_endpoint and abs(left_x - xs_cand[-1]) < tol: # At right endpoint
|
|
1452
|
+
# Only add if not duplicate of the endpoint being projected and not already in list
|
|
1453
|
+
if abs(y_cand - left_y) > tol and not point_exists(left_vertical_points, left_x, y_cand, tol):
|
|
1454
|
+
left_vertical_points.append((left_x, y_cand))
|
|
1455
|
+
else: # Not at endpoint, use as stopping point
|
|
1456
|
+
if y_cand > left_y_bot:
|
|
1457
|
+
left_y_bot = y_cand
|
|
1458
|
+
|
|
1459
|
+
# Check right endpoint projection
|
|
1370
1460
|
if xs_cand[0] - tol <= right_x <= xs_cand[-1] + tol:
|
|
1371
|
-
y_cand =
|
|
1372
|
-
if y_cand
|
|
1373
|
-
|
|
1461
|
+
y_cand, is_at_endpoint = find_lowest_y_at_x(lower_candidate, right_x, tol)
|
|
1462
|
+
if y_cand is not None:
|
|
1463
|
+
# If intersection is at the left end of the lower line, add point but continue
|
|
1464
|
+
if is_at_endpoint and abs(right_x - xs_cand[0]) < tol: # At left endpoint
|
|
1465
|
+
# Only add if not duplicate of the endpoint being projected and not already in list
|
|
1466
|
+
if abs(y_cand - right_y) > tol and not point_exists(right_vertical_points, right_x, y_cand, tol):
|
|
1467
|
+
right_vertical_points.append((right_x, y_cand))
|
|
1468
|
+
else: # Not at endpoint, use as stopping point
|
|
1469
|
+
if y_cand > right_y_bot:
|
|
1470
|
+
right_y_bot = y_cand
|
|
1374
1471
|
|
|
1375
1472
|
# If no lower profile at endpoints, use max_depth
|
|
1376
1473
|
if left_y_bot == -np.inf:
|
|
@@ -1378,6 +1475,30 @@ def build_polygons(slope_data, reinf_lines=None, debug=False):
|
|
|
1378
1475
|
if right_y_bot == -np.inf:
|
|
1379
1476
|
right_y_bot = max_depth if max_depth is not None else -np.inf
|
|
1380
1477
|
|
|
1478
|
+
# Deduplicate vertical points (remove points that are too close to each other)
|
|
1479
|
+
def deduplicate_points(points, tol=1e-8):
|
|
1480
|
+
"""Remove duplicate points within tolerance."""
|
|
1481
|
+
if not points:
|
|
1482
|
+
return []
|
|
1483
|
+
unique_points = [points[0]]
|
|
1484
|
+
for p in points[1:]:
|
|
1485
|
+
# Check if this point is too close to any existing unique point
|
|
1486
|
+
is_duplicate = False
|
|
1487
|
+
for up in unique_points:
|
|
1488
|
+
if abs(p[0] - up[0]) < tol and abs(p[1] - up[1]) < tol:
|
|
1489
|
+
is_duplicate = True
|
|
1490
|
+
break
|
|
1491
|
+
if not is_duplicate:
|
|
1492
|
+
unique_points.append(p)
|
|
1493
|
+
return unique_points
|
|
1494
|
+
|
|
1495
|
+
right_vertical_points = deduplicate_points(right_vertical_points, tol)
|
|
1496
|
+
left_vertical_points = deduplicate_points(left_vertical_points, tol)
|
|
1497
|
+
|
|
1498
|
+
# Sort vertical points: right edge top to bottom, left edge bottom to top
|
|
1499
|
+
right_vertical_points.sort(key=lambda p: -p[1]) # Sort by y descending (top to bottom)
|
|
1500
|
+
left_vertical_points.sort(key=lambda p: p[1]) # Sort by y ascending (bottom to top)
|
|
1501
|
+
|
|
1381
1502
|
# Build bottom boundary: right projection, intermediate points (right to left), left projection
|
|
1382
1503
|
# The bottom should go from right to left to close the polygon
|
|
1383
1504
|
bottom = []
|
|
@@ -1402,16 +1523,42 @@ def build_polygons(slope_data, reinf_lines=None, debug=False):
|
|
|
1402
1523
|
else:
|
|
1403
1524
|
# For the lowest polygon, bottom is at max_depth
|
|
1404
1525
|
# Only need endpoints - no intermediate points
|
|
1526
|
+
left_y_bot = max_depth if max_depth is not None else -np.inf
|
|
1527
|
+
right_y_bot = max_depth if max_depth is not None else -np.inf
|
|
1405
1528
|
bottom = []
|
|
1406
1529
|
bottom.append((right_x, max_depth))
|
|
1407
1530
|
bottom.append((left_x, max_depth))
|
|
1408
1531
|
|
|
1409
|
-
# Build polygon: top left-to-right,
|
|
1532
|
+
# Build polygon: top left-to-right, right vertical edge (with intermediate points),
|
|
1533
|
+
# bottom right-to-left, left vertical edge (with intermediate points)
|
|
1410
1534
|
poly = []
|
|
1535
|
+
|
|
1536
|
+
# Top edge: left to right along profile line
|
|
1411
1537
|
for x, y in zip(xs_top, ys_top):
|
|
1412
1538
|
poly.append((round(x, 6), round(y, 6)))
|
|
1539
|
+
|
|
1540
|
+
# Right vertical edge: from (right_x, right_y) down to (right_x, right_y_bot)
|
|
1541
|
+
# Include intermediate points where we intersect left endpoints of lower lines
|
|
1542
|
+
# Note: (right_x, right_y_bot) will be added as part of the bottom edge, so don't add it here
|
|
1543
|
+
if i < n - 1:
|
|
1544
|
+
for x, y in right_vertical_points:
|
|
1545
|
+
# Only add if it's between top and bottom (not duplicate of endpoints)
|
|
1546
|
+
if abs(y - right_y) > tol and abs(y - right_y_bot) > tol:
|
|
1547
|
+
poly.append((round(x, 6), round(y, 6)))
|
|
1548
|
+
|
|
1549
|
+
# Bottom edge: right to left (already includes (right_x, right_y_bot) and (left_x, left_y_bot))
|
|
1413
1550
|
for x, y in bottom:
|
|
1414
1551
|
poly.append((round(x, 6), round(y, 6)))
|
|
1552
|
+
|
|
1553
|
+
# Left vertical edge: from (left_x, left_y_bot) up to (left_x, left_y)
|
|
1554
|
+
# Include intermediate points where we intersect right endpoints of lower lines
|
|
1555
|
+
# Note: (left_x, left_y_bot) was already added as part of the bottom edge
|
|
1556
|
+
if i < n - 1:
|
|
1557
|
+
for x, y in reversed(left_vertical_points): # Reverse to go bottom to top
|
|
1558
|
+
# Only add if it's between bottom and top (not duplicate of endpoints)
|
|
1559
|
+
if abs(y - left_y_bot) > tol and abs(y - left_y) > tol:
|
|
1560
|
+
poly.append((round(x, 6), round(y, 6)))
|
|
1561
|
+
|
|
1415
1562
|
# Clean up polygon (should rarely do anything)
|
|
1416
1563
|
poly = clean_polygon(poly)
|
|
1417
1564
|
polygons.append(poly)
|
xslope/plot.py
CHANGED
|
@@ -86,8 +86,10 @@ def plot_max_depth(ax, profile_lines, max_depth):
|
|
|
86
86
|
x_max = max(x_vals)
|
|
87
87
|
ax.hlines(max_depth, x_min, x_max, colors='black', linewidth=1.5, label='Max Depth')
|
|
88
88
|
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
x_diff = x_max - x_min
|
|
90
|
+
spacing = x_diff / 100
|
|
91
|
+
length = x_diff / 80
|
|
92
|
+
|
|
91
93
|
angle_rad = np.radians(60)
|
|
92
94
|
dx = length * np.cos(angle_rad)
|
|
93
95
|
dy = length * np.sin(angle_rad)
|
xslope/plot_seep.py
CHANGED
|
@@ -217,20 +217,73 @@ def plot_seep_data(seep_data, figsize=(14, 6), show_nodes=False, show_bc=False,
|
|
|
217
217
|
plt.show()
|
|
218
218
|
|
|
219
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,
|
|
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, mesh=True, variable="head", vectors=False, vector_scale=0.05, flowlines=True):
|
|
221
221
|
"""
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
222
|
+
Plot seepage analysis results including head contours, flowlines, and phreatic surface.
|
|
223
|
+
|
|
224
|
+
This function visualizes the results of a seepage analysis by plotting contours of various
|
|
225
|
+
nodal variables (head, pore pressure, velocity magnitude, or gradient magnitude). When
|
|
226
|
+
plotting head, flowlines are also overlaid. The plot properly handles mesh aspect ratios
|
|
227
|
+
and supports both linear and quadratic triangular and quadrilateral elements.
|
|
228
|
+
|
|
229
|
+
Parameters:
|
|
230
|
+
-----------
|
|
231
|
+
seep_data : dict
|
|
232
|
+
Dictionary containing seepage mesh data from import_seep2d. Required keys include:
|
|
233
|
+
'nodes', 'elements', 'element_materials', 'element_types' (optional), and
|
|
234
|
+
'k1_by_mat' (optional, for flowline calculation).
|
|
235
|
+
solution : dict
|
|
236
|
+
Dictionary containing solution results from run_seepage_analysis. Required keys include:
|
|
237
|
+
'head' (array of total head values at nodes), 'velocity' (array of velocity vectors),
|
|
238
|
+
'gradient' (array of hydraulic gradient vectors). Optional keys: 'phi' (stream function),
|
|
239
|
+
'flowrate' (total flow rate), 'u' (pore pressure), 'v_mag' (velocity magnitude),
|
|
240
|
+
'i_mag' (gradient magnitude).
|
|
241
|
+
figsize : tuple of float, optional
|
|
242
|
+
Figure size in inches (width, height). Default is (14, 6).
|
|
243
|
+
levels : int, optional
|
|
244
|
+
Number of contour levels to plot. Default is 20.
|
|
245
|
+
base_mat : int, optional
|
|
246
|
+
Material ID (1-based) used to compute hydraulic conductivity for flow function
|
|
247
|
+
calculation. Default is 1. Only used when variable="head".
|
|
248
|
+
fill_contours : bool, optional
|
|
249
|
+
If True, shows filled contours with color map. If False, only black solid
|
|
250
|
+
contour lines are shown. Default is True.
|
|
251
|
+
phreatic : bool, optional
|
|
252
|
+
If True, plots the phreatic surface (where pressure head = 0) as a thick red line.
|
|
253
|
+
Default is True. Only applicable when variable="head".
|
|
254
|
+
alpha : float, optional
|
|
255
|
+
Transparency level (0-1) for material zone fill colors. Default is 0.4.
|
|
256
|
+
pad_frac : float, optional
|
|
257
|
+
Fraction of mesh extent to add as padding around the plot boundaries. Default is 0.05.
|
|
258
|
+
mesh : bool, optional
|
|
259
|
+
If True, overlays element edges in light gray. Default is True.
|
|
260
|
+
variable : str, optional
|
|
261
|
+
Nodal variable to contour. Options: "head" (default), "u" (pore pressure),
|
|
262
|
+
"v_mag" (velocity magnitude), "i_mag" (gradient magnitude). When "head" is selected,
|
|
263
|
+
flowlines can be overlaid if flowlines=True. Other variables do not include flowlines.
|
|
264
|
+
vectors : bool, optional
|
|
265
|
+
If True, plots velocity vectors as arrows at each node. Default is False.
|
|
266
|
+
vector_scale : float, optional
|
|
267
|
+
Scale factor for vector lengths. Maximum vector length will be x_range * vector_scale,
|
|
268
|
+
where x_range is the x-extent of the mesh. Default is 0.05.
|
|
269
|
+
flowlines : bool, optional
|
|
270
|
+
If True and variable="head", overlays flowlines (stream function contours) on the plot.
|
|
271
|
+
Default is True. Only applicable when variable="head".
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
--------
|
|
275
|
+
None
|
|
276
|
+
Displays the plot using matplotlib.pyplot.show().
|
|
277
|
+
|
|
278
|
+
Notes:
|
|
279
|
+
------
|
|
280
|
+
- The function automatically subdivides quadratic elements (tri6, quad8, quad9) for
|
|
281
|
+
proper visualization and contouring.
|
|
282
|
+
- Flowlines are only plotted when variable="head" and if 'phi' and 'flowrate' are present
|
|
283
|
+
in solution and 'k1_by_mat' is present in seep_data.
|
|
284
|
+
- The plot includes a colorbar for contours when fill_contours=True.
|
|
285
|
+
- The title includes flowrate information if available in the solution dictionary and
|
|
286
|
+
variable="head".
|
|
234
287
|
"""
|
|
235
288
|
import matplotlib.pyplot as plt
|
|
236
289
|
import matplotlib.tri as tri
|
|
@@ -238,15 +291,30 @@ def plot_seep_solution(seep_data, solution, figsize=(14, 6), levels=20, base_mat
|
|
|
238
291
|
from matplotlib.patches import Polygon
|
|
239
292
|
import numpy as np
|
|
240
293
|
|
|
294
|
+
# Validate variable parameter
|
|
295
|
+
valid_variables = ["head", "u", "v_mag", "i_mag"]
|
|
296
|
+
if variable not in valid_variables:
|
|
297
|
+
raise ValueError(f"variable must be one of {valid_variables}, got '{variable}'")
|
|
298
|
+
|
|
241
299
|
# Extract data from seep_data and solution
|
|
242
300
|
nodes = seep_data["nodes"]
|
|
243
301
|
elements = seep_data["elements"]
|
|
244
302
|
element_materials = seep_data["element_materials"]
|
|
245
303
|
element_types = seep_data.get("element_types", None) # New field for element types
|
|
246
304
|
k1_by_mat = seep_data.get("k1_by_mat") # Use .get() in case it's not present
|
|
305
|
+
|
|
306
|
+
# Extract the variable to plot
|
|
307
|
+
if variable not in solution:
|
|
308
|
+
raise ValueError(f"Variable '{variable}' not found in solution dictionary. Available keys: {list(solution.keys())}")
|
|
309
|
+
contour_data = solution[variable]
|
|
310
|
+
|
|
311
|
+
# Extract head and flowline-related data (only needed for head plots)
|
|
247
312
|
head = solution["head"]
|
|
248
313
|
phi = solution.get("phi")
|
|
249
314
|
flowrate = solution.get("flowrate")
|
|
315
|
+
|
|
316
|
+
# Determine if we should plot flowlines (only for head and if flowlines=True)
|
|
317
|
+
plot_flowlines = (variable == "head" and flowlines)
|
|
250
318
|
|
|
251
319
|
|
|
252
320
|
# Use constrained_layout for best layout
|
|
@@ -347,9 +415,9 @@ def plot_seep_solution(seep_data, solution, figsize=(14, 6), levels=20, base_mat
|
|
|
347
415
|
for sub_quad in sub_quads:
|
|
348
416
|
ax.fill(*zip(*sub_quad), edgecolor='none', facecolor=color, alpha=alpha)
|
|
349
417
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
418
|
+
# Set up contour levels
|
|
419
|
+
vmin = np.min(contour_data)
|
|
420
|
+
vmax = np.max(contour_data)
|
|
353
421
|
contour_levels = np.linspace(vmin, vmax, levels)
|
|
354
422
|
|
|
355
423
|
# For contouring, subdivide tri6 elements into 4 subtriangles
|
|
@@ -374,24 +442,41 @@ def plot_seep_solution(seep_data, solution, figsize=(14, 6), levels=20, base_mat
|
|
|
374
442
|
all_triangles_for_contouring.extend([tri1, tri2])
|
|
375
443
|
triang = tri.Triangulation(nodes[:, 0], nodes[:, 1], all_triangles_for_contouring)
|
|
376
444
|
|
|
445
|
+
# Variable labels for colorbar and title
|
|
446
|
+
variable_labels = {
|
|
447
|
+
"head": "Total Head",
|
|
448
|
+
"u": "Pore Pressure",
|
|
449
|
+
"v_mag": "Velocity Magnitude",
|
|
450
|
+
"i_mag": "Hydraulic Gradient Magnitude"
|
|
451
|
+
}
|
|
452
|
+
variable_label = variable_labels[variable]
|
|
453
|
+
|
|
377
454
|
# Filled contours (only if fill_contours=True)
|
|
378
455
|
if fill_contours:
|
|
379
|
-
contourf = ax.tricontourf(triang,
|
|
380
|
-
cbar = plt.colorbar(contourf, ax=ax, label=
|
|
456
|
+
contourf = ax.tricontourf(triang, contour_data, levels=contour_levels, cmap="Spectral_r", vmin=vmin, vmax=vmax, alpha=0.5)
|
|
457
|
+
cbar = plt.colorbar(contourf, ax=ax, label=variable_label, shrink=0.8, pad=0.02)
|
|
381
458
|
cbar.locator = MaxNLocator(nbins=10, steps=[1, 2, 5])
|
|
382
459
|
cbar.update_ticks()
|
|
383
460
|
|
|
384
|
-
# Solid lines for
|
|
385
|
-
ax.tricontour(triang,
|
|
386
|
-
|
|
387
|
-
# Phreatic surface (pressure head = 0)
|
|
388
|
-
if phreatic
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
461
|
+
# Solid lines for contours
|
|
462
|
+
ax.tricontour(triang, contour_data, levels=contour_levels, colors="k", linewidths=0.5)
|
|
463
|
+
|
|
464
|
+
# Phreatic surface (pressure head = 0) - only for head plots
|
|
465
|
+
# Check if phreatic surface exists (pore pressure must be negative somewhere)
|
|
466
|
+
has_phreatic = False
|
|
467
|
+
if phreatic and plot_flowlines:
|
|
468
|
+
# Check if pore pressure goes negative (indicating a phreatic surface exists)
|
|
469
|
+
u = solution.get("u")
|
|
470
|
+
if u is not None and np.min(u) < 0:
|
|
471
|
+
elevation = nodes[:, 1] # y-coordinate is elevation
|
|
472
|
+
pressure_head = head - elevation
|
|
473
|
+
ax.tricontour(triang, pressure_head, levels=[0], colors="red", linewidths=2.0)
|
|
474
|
+
has_phreatic = True
|
|
475
|
+
|
|
476
|
+
# Overlay flowlines if variable is head and phi is available
|
|
477
|
+
if plot_flowlines and phi is not None and flowrate is not None and k1_by_mat is not None:
|
|
478
|
+
# Compute head drop for flowline calculation
|
|
479
|
+
hdrop = vmax - vmin
|
|
395
480
|
if base_mat > len(k1_by_mat):
|
|
396
481
|
print(f"Warning: base_mat={base_mat} is larger than number of materials ({len(k1_by_mat)}). Using material 1.")
|
|
397
482
|
base_mat = 1
|
|
@@ -406,8 +491,39 @@ def plot_seep_solution(seep_data, solution, figsize=(14, 6), levels=20, base_mat
|
|
|
406
491
|
phi_contours = np.linspace(np.min(phi), np.max(phi), phi_levels)
|
|
407
492
|
ax.tricontour(triang, phi, levels=phi_contours, colors="blue", linewidths=0.7, linestyles="solid")
|
|
408
493
|
|
|
494
|
+
# Plot velocity vectors if requested
|
|
495
|
+
if vectors:
|
|
496
|
+
velocity = solution.get("velocity")
|
|
497
|
+
if velocity is not None:
|
|
498
|
+
# Calculate x_range for scaling
|
|
499
|
+
x_min_vec = nodes[:, 0].min()
|
|
500
|
+
x_max_vec = nodes[:, 0].max()
|
|
501
|
+
x_range = x_max_vec - x_min_vec
|
|
502
|
+
max_vector_length = x_range * vector_scale
|
|
503
|
+
|
|
504
|
+
# Get velocity magnitude
|
|
505
|
+
v_mag = solution.get("v_mag")
|
|
506
|
+
if v_mag is None:
|
|
507
|
+
# Calculate v_mag if not available
|
|
508
|
+
v_mag = np.linalg.norm(velocity, axis=1)
|
|
509
|
+
|
|
510
|
+
# Find maximum velocity magnitude
|
|
511
|
+
max_v_mag = np.max(v_mag)
|
|
512
|
+
|
|
513
|
+
# Scale vectors: if max_v_mag > 0, scale so max vector has length max_vector_length
|
|
514
|
+
if max_v_mag > 0:
|
|
515
|
+
scale_factor = max_vector_length / max_v_mag
|
|
516
|
+
velocity_scaled = velocity * scale_factor
|
|
517
|
+
else:
|
|
518
|
+
velocity_scaled = velocity
|
|
519
|
+
|
|
520
|
+
# Plot vectors using quiver
|
|
521
|
+
ax.quiver(nodes[:, 0], nodes[:, 1], velocity_scaled[:, 0], velocity_scaled[:, 1],
|
|
522
|
+
angles='xy', scale_units='xy', scale=1, width=0.002, headwidth=2.5,
|
|
523
|
+
headlength=3, headaxislength=2.5, color='black', alpha=0.7)
|
|
524
|
+
|
|
409
525
|
# Plot element edges if requested
|
|
410
|
-
if
|
|
526
|
+
if mesh:
|
|
411
527
|
# Draw all element edges
|
|
412
528
|
for element, elem_type in zip(elements, element_types if element_types is not None else [3]*len(elements)):
|
|
413
529
|
if elem_type == 3:
|
|
@@ -444,13 +560,17 @@ def plot_seep_solution(seep_data, solution, figsize=(14, 6), levels=20, base_mat
|
|
|
444
560
|
ax.set_xlim(x_min - x_pad, x_max + x_pad)
|
|
445
561
|
ax.set_ylim(y_min - y_pad, y_max + y_pad)
|
|
446
562
|
|
|
447
|
-
|
|
448
|
-
if
|
|
449
|
-
title
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
563
|
+
# Build title based on variable
|
|
564
|
+
if variable == "head":
|
|
565
|
+
title = f"Flow Net: {variable_label} Contours"
|
|
566
|
+
if plot_flowlines and phi is not None:
|
|
567
|
+
title += " and Flowlines"
|
|
568
|
+
if has_phreatic:
|
|
569
|
+
title += " with Phreatic Surface"
|
|
570
|
+
if flowrate is not None:
|
|
571
|
+
title += f" — Total Flowrate: {flowrate:.3f}"
|
|
572
|
+
else:
|
|
573
|
+
title = f"{variable_label} Contours"
|
|
454
574
|
ax.set_title(title)
|
|
455
575
|
|
|
456
576
|
# Set equal aspect ratio AFTER setting limits
|