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/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 lists of (x, y) tuples defining material boundaries
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
- # build a list of region ids (list of material IDs - one per polygon)
96
- region_ids = [i for i in range(len(polygons))]
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(polygons, region_ids)):
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(polygons, region_ids)):
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, each defined by (x,y) coordinate tuples
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
- polygons.append(poly)
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 (lists of (x,y) tuples)
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
- updated_poly = list(poly) # Make a copy
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
- updated_polygons.append(updated_poly)
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
- print(f"Material Zone {i+1} (Material ID: {i}):")
1797
- print(f" Number of vertices: {len(polygon)}")
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(polygon) - 1):
1802
- x1, y1 = polygon[j]
1803
- x2, y2 = polygon[j + 1]
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 polygon]
1811
- ys = [y for x, y in polygon]
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
- updated_polygons.append(list(poly)) # Convert to list for modification
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(poly)):
3020
- poly_edge_start = poly[j]
3021
- poly_edge_end = poly[(j + 1) % len(poly)]
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 poly:
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].insert(insert_idx, intersection)
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 coordinates representing material boundaries
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
- xs, ys = zip(*line)
70
- ax.plot(xs, ys, color=get_material_color(i), linewidth=1, label=f'Profile {i+1}')
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 coordinates representing material boundaries
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 seepage boundary-condition lines for seepage-only workflows.
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 seepage material properties table to the plot.
812
+ Adds a seep material properties table to the plot.
691
813
 
692
- Displays hydraulic properties for seepage analysis including hydraulic conductivities
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 seepage material properties with keys:
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
- if hasattr(line, "xy"):
1040
- _, ys = line.xy
1161
+ coords = line['coords']
1162
+ if hasattr(coords, "xy"):
1163
+ _, ys = coords.xy
1041
1164
  else:
1042
- _, ys = zip(*line)
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(polygons, materials=None, nodes=False, legend=True, title="Material Zone Polygons", save_png=False, dpi=300):
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=(12, 8))
2040
+ fig, ax = plt.subplots(figsize=figsize)
1908
2041
  for i, polygon in enumerate(polygons):
1909
- xs = [x for x, y in polygon]
1910
- ys = [y for x, y in polygon]
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 i < len(materials):
1913
- item = materials[i]
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 {i}"
1919
- ax.fill(xs, ys, color=get_material_color(i), alpha=0.6, label=label)
1920
- ax.plot(xs, ys, color=get_material_color(i), linewidth=1)
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
- xs = [x for x, y in polygon]
1955
- ys = [y for x, y in polygon]
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
- ax.fill(xs, ys, color=get_material_color(i), alpha=0.6, label=f'Material {i}')
1958
- ax.plot(xs, ys, color=get_material_color(i), linewidth=1)
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 i < len(materials):
1964
- item = materials[i]
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 {i}: {mat_name}')
2111
+ ax.set_title(f'Material {mat_idx}: {mat_name}')
1971
2112
  else:
1972
- ax.set_title(f'Material {i}')
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
- xs = [p[0] for p in line]
2062
- ys = [p[1] for p in line]
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 seepage data from import_seep2d
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"SEEP2D Mesh with Material Zones ({num_triangles} triangles, {num_quads} quads)"
209
+ title = f"Finite Element Mesh with Material Zones ({num_triangles} triangles, {num_quads} quads)"
210
210
  elif num_quads > 0:
211
- title = f"SEEP2D Mesh with Material Zones ({num_quads} quadrilaterals)"
211
+ title = f"Finite Element Mesh with Material Zones ({num_quads} quadrilaterals)"
212
212
  else:
213
- title = f"SEEP2D Mesh with Material Zones ({num_triangles} triangles)"
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 seepage analysis results including head contours, flowlines, and phreatic surface.
228
+ Plot seep analysis results including head contours, flowlines, and phreatic surface.
229
229
 
230
- This function visualizes the results of a seepage analysis by plotting contours of various
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 seepage mesh data from import_seep2d. Required keys include:
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