xslope 0.1.11__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/plot_fem.py CHANGED
@@ -22,7 +22,7 @@ from matplotlib.colors import LinearSegmentedColormap
22
22
  from matplotlib.patches import Polygon
23
23
 
24
24
 
25
- def plot_fem_data(fem_data, figsize=(14, 6), show_nodes=False, show_bc=True, material_table=False,
25
+ def plot_fem_data(fem_data, figsize=(14, 6), show_nodes=False, show_bc=True,
26
26
  label_elements=False, label_nodes=False, alpha=0.4, bc_symbol_size=0.03, save_png=False, dpi=300):
27
27
  """
28
28
  Plots a FEM mesh colored by material zone with boundary conditions displayed.
@@ -32,7 +32,6 @@ def plot_fem_data(fem_data, figsize=(14, 6), show_nodes=False, show_bc=True, mat
32
32
  figsize: Figure size
33
33
  show_nodes: If True, plot node points
34
34
  show_bc: If True, plot boundary condition symbols
35
- material_table: If True, show material table
36
35
  label_elements: If True, label each element with its number at its centroid
37
36
  label_nodes: If True, label each node with its number just above and to the right
38
37
  alpha: Transparency for element faces
@@ -245,10 +244,6 @@ def plot_fem_data(fem_data, figsize=(14, 6), show_nodes=False, show_bc=True, mat
245
244
  else:
246
245
  title = f"FEM Mesh with Material Zones ({num_triangles} triangles)"
247
246
 
248
- # Place the table in the upper left
249
- if material_table:
250
- _plot_fem_material_table(ax, fem_data, xloc=0.3, yloc=1.1) # upper left
251
-
252
247
  ax.set_title(title)
253
248
  plt.tight_layout()
254
249
 
@@ -366,69 +361,6 @@ def _plot_boundary_conditions(ax, nodes, bc_type, bc_values, legend_handles, bc_
366
361
  markersize=8, label='Applied Force (bc_type=4)')
367
362
  )
368
363
 
369
-
370
- def _plot_fem_material_table(ax, fem_data, xloc=0.6, yloc=0.7):
371
- """
372
- Adds a FEM material properties table to the plot.
373
-
374
- Parameters:
375
- ax: matplotlib Axes object
376
- fem_data: Dictionary containing FEM data with material properties
377
- xloc: x-location of table (0-1)
378
- yloc: y-location of table (0-1)
379
-
380
- Returns:
381
- None
382
- """
383
- # Extract material properties from fem_data
384
- c_by_mat = fem_data.get("c_by_mat")
385
- phi_by_mat = fem_data.get("phi_by_mat")
386
- E_by_mat = fem_data.get("E_by_mat")
387
- nu_by_mat = fem_data.get("nu_by_mat")
388
- gamma_by_mat = fem_data.get("gamma_by_mat")
389
- material_names = fem_data.get("material_names", [])
390
-
391
- if c_by_mat is None or len(c_by_mat) == 0:
392
- return
393
-
394
- # Column headers for FEM properties
395
- col_labels = ["Mat", "Name", "γ", "c", "φ", "E", "ν"]
396
-
397
- # Build table rows
398
- table_data = []
399
- for idx in range(len(c_by_mat)):
400
- c = c_by_mat[idx]
401
- phi = phi_by_mat[idx] if phi_by_mat is not None else 0.0
402
- E = E_by_mat[idx] if E_by_mat is not None else 0.0
403
- nu = nu_by_mat[idx] if nu_by_mat is not None else 0.0
404
- gamma = gamma_by_mat[idx] if gamma_by_mat is not None else 0.0
405
-
406
- # Get material name, use default if not available
407
- material_name = material_names[idx] if idx < len(material_names) else f"Material {idx+1}"
408
-
409
- # Format values with appropriate precision
410
- row = [
411
- idx + 1, # Material number (1-based)
412
- material_name, # Material name
413
- f"{gamma:.1f}", # unit weight
414
- f"{c:.1f}", # cohesion
415
- f"{phi:.1f}", # friction angle
416
- f"{E:.0f}", # Young's modulus
417
- f"{nu:.2f}" # Poisson's ratio
418
- ]
419
- table_data.append(row)
420
-
421
- # Add the table
422
- table = ax.table(cellText=table_data,
423
- colLabels=col_labels,
424
- loc='upper right',
425
- colLoc='center',
426
- cellLoc='center',
427
- bbox=[xloc, yloc, 0.45, 0.25]) # Increased width to accommodate name column
428
- table.auto_set_font_size(False)
429
- table.set_fontsize(8)
430
-
431
-
432
364
  def plot_fem_results(fem_data, solution, plot_type='displacement', deform_scale=None,
433
365
  show_mesh=True, show_reinforcement=True, figsize=(12, 8), label_elements=False,
434
366
  plot_nodes=False, plot_elements=False, plot_boundary=True, displacement_tolerance=0.5,
xslope/plot_seep.py CHANGED
@@ -4,16 +4,15 @@ from matplotlib.ticker import MaxNLocator
4
4
  import numpy as np
5
5
 
6
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, save_png=False, dpi=300):
7
+ def plot_seep_data(seep_data, figsize=(14, 6), show_nodes=False, show_bc=False, label_elements=False, label_nodes=False, alpha=0.4, save_png=False, dpi=300):
8
8
  """
9
9
  Plots a mesh colored by material zone.
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
- material_table: If True, show material table
17
16
  label_elements: If True, label each element with its number at its centroid
18
17
  label_nodes: If True, label each node with its number just above and to the right
19
18
  """
@@ -181,10 +180,10 @@ def plot_seep_data(seep_data, figsize=(14, 6), show_nodes=False, show_bc=False,
181
180
  bc1 = nodes[bc_type == 1]
182
181
  bc2 = nodes[bc_type == 2]
183
182
  if len(bc1) > 0:
184
- h1, = ax.plot(bc1[:, 0], bc1[:, 1], 'ro', label="Fixed Head (bc_type=1)")
183
+ h1, = ax.plot(bc1[:, 0], bc1[:, 1], 'bs', label="Fixed Head (bc_type=1)")
185
184
  legend_handles.append(h1)
186
185
  if len(bc2) > 0:
187
- h2, = ax.plot(bc2[:, 0], bc2[:, 1], 'bs', label="Exit Face (bc_type=2)")
186
+ h2, = ax.plot(bc2[:, 0], bc2[:, 1], 'ro', label="Exit Face (bc_type=2)")
188
187
  legend_handles.append(h2)
189
188
 
190
189
  # Single combined legend outside the plot
@@ -196,20 +195,22 @@ def plot_seep_data(seep_data, figsize=(14, 6), show_nodes=False, show_bc=False,
196
195
  frameon=False
197
196
  )
198
197
  ax.set_aspect("equal")
198
+
199
+ # Add a bit of headroom so the mesh/BC markers don't touch the top border
200
+ y0, y1 = ax.get_ylim()
201
+ if y1 > y0:
202
+ pad = 0.05 * (y1 - y0)
203
+ ax.set_ylim(y0, y1 + pad)
199
204
 
200
205
  # Count element types for title
201
206
  num_triangles = np.sum(element_types == 3)
202
207
  num_quads = np.sum(element_types == 4)
203
208
  if num_triangles > 0 and num_quads > 0:
204
- 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)"
205
210
  elif num_quads > 0:
206
- title = f"SEEP2D Mesh with Material Zones ({num_quads} quadrilaterals)"
211
+ title = f"Finite Element Mesh with Material Zones ({num_quads} quadrilaterals)"
207
212
  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
+ title = f"Finite Element Mesh with Material Zones ({num_triangles} triangles)"
213
214
 
214
215
  ax.set_title(title)
215
216
  # plt.subplots_adjust(bottom=0.2) # Add vertical cushion
@@ -224,9 +225,9 @@ def plot_seep_data(seep_data, figsize=(14, 6), show_nodes=False, show_bc=False,
224
225
 
225
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):
226
227
  """
227
- Plot seepage analysis results including head contours, flowlines, and phreatic surface.
228
+ Plot seep analysis results including head contours, flowlines, and phreatic surface.
228
229
 
229
- 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
230
231
  nodal variables (head, pore pressure, velocity magnitude, or gradient magnitude). When
231
232
  plotting head, flowlines are also overlaid. The plot properly handles mesh aspect ratios
232
233
  and supports both linear and quadratic triangular and quadrilateral elements.
@@ -234,7 +235,7 @@ def plot_seep_solution(seep_data, solution, figsize=(14, 6), levels=20, base_mat
234
235
  Parameters:
235
236
  -----------
236
237
  seep_data : dict
237
- Dictionary containing seepage mesh data from import_seep2d. Required keys include:
238
+ Dictionary containing seep mesh data from import_seep2d. Required keys include:
238
239
  'nodes', 'elements', 'element_materials', 'element_types' (optional), and
239
240
  'k1_by_mat' (optional, for flowline calculation).
240
241
  solution : dict
@@ -592,66 +593,7 @@ def plot_seep_solution(seep_data, solution, figsize=(14, 6), levels=20, base_mat
592
593
  plt.show()
593
594
 
594
595
 
595
- def plot_seep_material_table(ax, seep_data, xloc=0.6, yloc=0.7):
596
- """
597
- Adds a seepage material properties table to the plot.
598
-
599
- Parameters:
600
- ax: matplotlib Axes object
601
- seep_data: Dictionary containing seepage data with material properties
602
- xloc: x-location of table (0-1)
603
- yloc: y-location of table (0-1)
604
-
605
- Returns:
606
- None
607
- """
608
- # Extract material properties from seep_data
609
- k1_by_mat = seep_data.get("k1_by_mat")
610
- k2_by_mat = seep_data.get("k2_by_mat")
611
- angle_by_mat = seep_data.get("angle_by_mat")
612
- kr0_by_mat = seep_data.get("kr0_by_mat")
613
- h0_by_mat = seep_data.get("h0_by_mat")
614
- material_names = seep_data.get("material_names", [])
615
-
616
- if k1_by_mat is None or len(k1_by_mat) == 0:
617
- return
618
-
619
- # Column headers for seepage properties
620
- col_labels = ["Mat", "Name", "k₁", "k₂", "Angle", "kr₀", "h₀"]
621
-
622
- # Build table rows
623
- table_data = []
624
- for idx in range(len(k1_by_mat)):
625
- k1 = k1_by_mat[idx]
626
- k2 = k2_by_mat[idx] if k2_by_mat is not None else 0.0
627
- angle = angle_by_mat[idx] if angle_by_mat is not None else 0.0
628
- kr0 = kr0_by_mat[idx] if kr0_by_mat is not None else 0.0
629
- h0 = h0_by_mat[idx] if h0_by_mat is not None else 0.0
630
-
631
- # Get material name, use default if not available
632
- material_name = material_names[idx] if idx < len(material_names) else f"Material {idx+1}"
633
-
634
- # Format values with appropriate precision
635
- row = [
636
- idx + 1, # Material number (1-based)
637
- material_name, # Material name
638
- f"{k1:.3f}", # k1 in scientific notation
639
- f"{k2:.3f}", # k2 in scientific notation
640
- f"{angle:.1f}", # angle in degrees
641
- f"{kr0:.4f}", # kr0
642
- f"{h0:.2f}" # h0
643
- ]
644
- table_data.append(row)
645
-
646
- # Add the table
647
- table = ax.table(cellText=table_data,
648
- colLabels=col_labels,
649
- loc='upper right',
650
- colLoc='center',
651
- cellLoc='center',
652
- bbox=[xloc, yloc, 0.45, 0.25]) # Increased width to accommodate name column
653
- table.auto_set_font_size(False)
654
- table.set_fontsize(8)
596
+ # plot_seep_material_table has been moved to xslope/plot.py
655
597
 
656
598
 
657
599
  def get_ordered_mesh_boundary(nodes, elements, element_types=None):
xslope/seep.py CHANGED
@@ -24,13 +24,13 @@ def build_seep_data(mesh, slope_data):
24
24
  Build a seep_data dictionary from a mesh and data dictionary.
25
25
 
26
26
  This function takes a mesh dictionary (from build_mesh_from_polygons) and a data dictionary
27
- (from load_slope_data) and constructs a seep_data dictionary suitable for seepage analysis.
27
+ (from load_slope_data) and constructs a seep_data dictionary suitable for seep analysis.
28
28
 
29
29
  The function:
30
30
  1. Extracts mesh information (nodes, elements, element types, element materials)
31
31
  2. Builds material property arrays (k1, k2, alpha, kr0, h0) from the materials table
32
32
  3. Constructs boundary conditions by finding nodes that intersect with specified head
33
- and seepage face lines from the data dictionary
33
+ and seep face lines from the data dictionary
34
34
 
35
35
  Parameters:
36
36
  mesh (dict): Mesh dictionary from build_mesh_from_polygons containing:
@@ -122,7 +122,7 @@ def build_seep_data(mesh, slope_data):
122
122
  bc_type[i] = 1 # Fixed head
123
123
  bc_values[i] = head_value
124
124
 
125
- # Process seepage face (exit face) boundary conditions
125
+ # Process seep face (exit face) boundary conditions
126
126
  exit_face_coords = seepage_bc.get("exit_face", [])
127
127
  if len(exit_face_coords) >= 2:
128
128
  # Create LineString from exit face coordinates
@@ -286,7 +286,7 @@ def import_seep2d(filepath):
286
286
 
287
287
  def solve_confined(nodes, elements, bc_type, dirichlet_bcs, k1_vals, k2_vals, angles=None, element_types=None):
288
288
  """
289
- FEM solver for confined seepage with anisotropic conductivity.
289
+ FEM solver for confined seep with anisotropic conductivity.
290
290
  Supports triangular and quadrilateral elements with both linear and quadratic shape functions.
291
291
 
292
292
  Parameters:
@@ -404,7 +404,7 @@ def solve_confined(nodes, elements, bc_type, dirichlet_bcs, k1_vals, k2_vals, an
404
404
 
405
405
  def solve_unsaturated(nodes, elements, bc_type, bc_values, kr0=0.001, h0=-1.0,
406
406
  k1_vals=1.0, k2_vals=1.0, angles=0.0,
407
- max_iter=200, tol=1e-4, element_types=None):
407
+ max_iter=200, tol=1e-6, element_types=None):
408
408
  """
409
409
  Iterative FEM solver for unconfined flow using linear kr frontal function.
410
410
  Supports triangular and quadrilateral elements with both linear and quadratic shape functions.
@@ -716,7 +716,7 @@ def solve_unsaturated(nodes, elements, bc_type, bc_values, kr0=0.001, h0=-1.0,
716
716
  print(" - Non-conservative flow field")
717
717
  print(" - Incorrect boundary identification")
718
718
  print(" - Numerical issues in the flow solution")
719
-
719
+ print(f"Try reducing the tolerance (tol) parameter. Current value: {tol:.6e}")
720
720
 
721
721
  return h, A, q_final, total_inflow
722
722
 
@@ -969,10 +969,14 @@ def create_flow_potential_bc(nodes, elements, q, debug=False, element_types=None
969
969
  starting_phi = phi[ordered_nodes[start_idx]]
970
970
  closure_error = phi_val - starting_phi
971
971
 
972
- if debug or abs(closure_error) > 1e-3:
972
+ # Use a relative threshold based on total positive boundary flow
973
+ rel_tol = 1e-2 # 1%
974
+ scale = max(total_q, 1e-12)
975
+
976
+ if debug or abs(closure_error) > rel_tol * scale:
973
977
  print(f"Flow potential closure check: error = {closure_error:.6e}")
974
978
 
975
- if abs(closure_error) > 1e-3:
979
+ if abs(closure_error) > rel_tol * scale:
976
980
  print(f"Warning: Large flow potential closure error = {closure_error:.6e}")
977
981
  print("This may indicate:")
978
982
  print(" - Non-conservative flow field")
@@ -1983,12 +1987,12 @@ def quad4_stiffness_matrix(nodes_elem, Kmat):
1983
1987
 
1984
1988
  return ke
1985
1989
 
1986
- def run_seepage_analysis(seep_data):
1990
+ def run_seepage_analysis(seep_data, tol=1e-6):
1987
1991
  """
1988
- Standalone function to run seepage analysis.
1992
+ Standalone function to run seep analysis.
1989
1993
 
1990
1994
  Args:
1991
- seep_data: Dictionary containing all the seepage data
1995
+ seep_data: Dictionary containing all the seep data
1992
1996
 
1993
1997
  Returns:
1994
1998
  Dictionary containing solution results with the following keys:
@@ -2019,7 +2023,7 @@ def run_seepage_analysis(seep_data):
2019
2023
  # Determine if unconfined flow
2020
2024
  is_unconfined = np.any(bc_type == 2)
2021
2025
  flow_type = "unconfined" if is_unconfined else "confined"
2022
- print(f"Solving {flow_type.upper()} seepage problem...")
2026
+ print(f"Solving {flow_type.upper()} seep problem...")
2023
2027
  print("Number of fixed-head nodes:", np.sum(bc_type == 1))
2024
2028
  print("Number of exit face nodes:", np.sum(bc_type == 2))
2025
2029
 
@@ -2048,7 +2052,8 @@ def run_seepage_analysis(seep_data):
2048
2052
  k1_vals=k1,
2049
2053
  k2_vals=k2,
2050
2054
  angles=angle,
2051
- element_types=element_types
2055
+ element_types=element_types,
2056
+ tol=tol
2052
2057
  )
2053
2058
  # Solve for potential function φ for flow lines
2054
2059
  dirichlet_phi_bcs = create_flow_potential_bc(nodes, elements, q, element_types=element_types)
@@ -2105,7 +2110,7 @@ def export_seep_solution(seep_data, solution, filename):
2105
2110
 
2106
2111
  Args:
2107
2112
  filename: Path to the output CSV file
2108
- seep_data: Dictionary containing seepage data
2113
+ seep_data: Dictionary containing seep data
2109
2114
  solution: Dictionary containing solution results from run_seepage_analysis
2110
2115
  """
2111
2116
  import pandas as pd
@@ -2135,7 +2140,7 @@ def print_seep_data_diagnostics(seep_data):
2135
2140
  Diagnostic function to print out the contents of seep_data after loading.
2136
2141
 
2137
2142
  Args:
2138
- seep_data: Dictionary containing seepage data
2143
+ seep_data: Dictionary containing seep data
2139
2144
  """
2140
2145
  print("\n" + "="*60)
2141
2146
  print("SEEP DATA DIAGNOSTICS")
xslope/slice.py CHANGED
@@ -122,7 +122,7 @@ def get_profile_layer_y_coordinates(x_coords, profile_lines):
122
122
 
123
123
  Parameters:
124
124
  x_coords (array-like): X-coordinates to evaluate
125
- profile_lines (list): List of profile layer lines
125
+ profile_lines (list): List of profile line dicts, each with 'coords' key containing coordinate tuples
126
126
 
127
127
  Returns:
128
128
  list: List of arrays, each containing y-coordinates for a profile layer
@@ -131,7 +131,7 @@ def get_profile_layer_y_coordinates(x_coords, profile_lines):
131
131
  layer_y_coords = []
132
132
 
133
133
  for line in profile_lines:
134
- line_coords = np.array(line)
134
+ line_coords = np.array(line['coords'])
135
135
  line_x = line_coords[:, 0]
136
136
  line_y = line_coords[:, 1]
137
137
 
@@ -534,7 +534,7 @@ def generate_slices(slope_data, circle=None, non_circ=None, num_slices=40, debug
534
534
 
535
535
  # Vectorized approach for profile line points
536
536
  for line in profile_lines:
537
- line_coords = np.array(line)
537
+ line_coords = np.array(line['coords'])
538
538
  x_coords = line_coords[:, 0]
539
539
  y_coords = line_coords[:, 1]
540
540
 
@@ -591,7 +591,7 @@ def generate_slices(slope_data, circle=None, non_circ=None, num_slices=40, debug
591
591
  # For circular failure surfaces, we can use a more efficient approach
592
592
  # by creating a dense circle representation and finding intersections
593
593
  for line in profile_lines:
594
- line_geom = LineString(line)
594
+ line_geom = LineString(line['coords'])
595
595
  # Create a dense circle representation for intersection
596
596
  theta_range = np.linspace(np.pi, 2 * np.pi, 200)
597
597
  circle_coords = [(Xo + R * np.cos(t), Yo + R * np.sin(t)) for t in theta_range]
@@ -608,7 +608,7 @@ def generate_slices(slope_data, circle=None, non_circ=None, num_slices=40, debug
608
608
  else:
609
609
  # For non-circular failure surfaces, use the original approach
610
610
  for i in range(len(profile_lines)):
611
- intersection = LineString(profile_lines[i]).intersection(clipped_surface)
611
+ intersection = LineString(profile_lines[i]['coords']).intersection(clipped_surface)
612
612
  if not intersection.is_empty:
613
613
  if hasattr(intersection, 'x'):
614
614
  fixed_xs.add(intersection.x)
@@ -775,12 +775,12 @@ def generate_slices(slope_data, circle=None, non_circ=None, num_slices=40, debug
775
775
  sum_gam_h_y = 0 # for calculating center of gravity of slice
776
776
  sum_gam_h = 0 # ditto
777
777
 
778
- for mat_index, layer_y in enumerate(profile_y_coords):
778
+ for profile_idx, layer_y in enumerate(profile_y_coords):
779
779
  layer_top_y = layer_y[i]
780
780
 
781
781
  # Bottom: highest of all other profile lines at x, or failure surface
782
782
  layer_bot_y = y_cb # Start with failure surface as default bottom
783
- for j in range(mat_index + 1, len(profile_y_coords)):
783
+ for j in range(profile_idx + 1, len(profile_y_coords)):
784
784
  next_y = profile_y_coords[j][i]
785
785
  if not np.isnan(next_y) and next_y > layer_bot_y:
786
786
  # Take the highest of the lower profile lines
@@ -792,14 +792,36 @@ def generate_slices(slope_data, circle=None, non_circ=None, num_slices=40, debug
792
792
  overlap_top = min(y_ct, layer_top_y)
793
793
  overlap_bot = max(y_cb, layer_bot_y)
794
794
  h = max(0, overlap_top - overlap_bot)
795
+
796
+ # Get material index from mat_id in profile line (already 0-based)
797
+ mat_id = profile_lines[profile_idx].get('mat_id')
798
+ if mat_id is not None and 0 <= mat_id < len(materials):
799
+ mat_index = mat_id
800
+ else:
801
+ # Fallback to profile index if no mat_id or out of range
802
+ mat_index = profile_idx
803
+
795
804
  sum_gam_h_y += h * materials[mat_index]['gamma'] * (overlap_top + overlap_bot) / 2
796
805
  sum_gam_h += h * materials[mat_index]['gamma']
797
806
 
798
807
  heights.append(h)
808
+
809
+ # Get material index for soil weight calculation (already 0-based)
810
+ mat_id = profile_lines[profile_idx].get('mat_id')
811
+ if mat_id is not None and 0 <= mat_id < len(materials):
812
+ mat_index = mat_id
813
+ else:
814
+ mat_index = profile_idx
815
+
799
816
  soil_weight += h * materials[mat_index]['gamma'] * dx
800
817
 
801
818
  if h > 0:
802
- base_material_idx = mat_index
819
+ # Get material index for base material (already 0-based)
820
+ mat_id = profile_lines[profile_idx].get('mat_id')
821
+ if mat_id is not None and 0 <= mat_id < len(materials):
822
+ base_material_idx = mat_id
823
+ else:
824
+ base_material_idx = profile_idx
803
825
 
804
826
  # Center of gravity
805
827
  y_cg = (sum_gam_h_y) / sum_gam_h if sum_gam_h > 0 else None
@@ -926,7 +948,7 @@ def generate_slices(slope_data, circle=None, non_circ=None, num_slices=40, debug
926
948
  else:
927
949
  u = 0
928
950
 
929
- # Check for second seepage solution (rapid drawdown)
951
+ # Check for second seep solution (rapid drawdown)
930
952
  if 'seep_u2' in data:
931
953
  seep_mesh = data['seep_mesh']
932
954
  seep_u2 = data['seep_u2']
xslope/solve.py CHANGED
@@ -768,13 +768,17 @@ def spencer(slice_df, tol=1e-4, max_iter = 100, debug_level=0):
768
768
  """Compute Q and y_Q for given F and theta values."""
769
769
  # Equation (24): m_alpha
770
770
  ma = 1 / (np.cos(alpha - theta_rad) + np.sin(alpha - theta_rad) * tan_p / F)
771
-
771
+
772
772
  # Equation (23): Q
773
773
  Q = (- Fv * sin_a - Fh * cos_a - (c / F) * dl + (Fv * cos_a - Fh * sin_a + u * dl) * tan_p / F) * ma
774
-
775
- # Equation (26): y_Q
776
- y_q = y_b + Mo / (Q * np.cos(theta_rad))
777
-
774
+
775
+ # Equation (26): y_Q with numerical safeguard
776
+ # Add small epsilon to prevent divide-by-zero when Q * cos(theta) is very small
777
+ Q_cos_theta = Q * np.cos(theta_rad)
778
+ eps = 1e-10
779
+ safe_denom = np.where(np.abs(Q_cos_theta) < eps, eps * np.sign(Q_cos_theta + eps), Q_cos_theta)
780
+ y_q = y_b + Mo / safe_denom
781
+
778
782
  return Q, y_q
779
783
 
780
784
  def compute_residuals(F, theta_rad):
@@ -814,10 +818,18 @@ def spencer(slice_df, tol=1e-4, max_iter = 100, debug_level=0):
814
818
  dC3_dtheta = sin_alpha_theta # Equation (55)
815
819
  dC4_dtheta = -cos_alpha_theta * tan_p # Equation (56)
816
820
  dQ_dtheta = (-1 / denom_Q**2) * (C1 + C2 / F) * (dC3_dtheta + dC4_dtheta / F)
817
-
821
+
818
822
  # Partial derivatives of y_Q (Equations 59-60)
819
- dyQ_dF = (-1 / (Q * cos_theta)**2) * Mo * dQ_dF * cos_theta
820
- dyQ_dtheta = (-1 / (Q * cos_theta)**2) * Mo * (dQ_dtheta * cos_theta - Q * sin_theta)
823
+ # Add numerical safeguard to prevent divide-by-zero when Q * cos_theta is very small
824
+ Q_cos_theta = Q * cos_theta
825
+ eps = 1e-10 # Small epsilon to prevent exact zero division
826
+
827
+ # Use np.where to handle element-wise operations safely
828
+ # Where |Q * cos_theta| < eps, set derivatives to a large value to signal ill-conditioning
829
+ safe_denom = np.where(np.abs(Q_cos_theta) < eps, eps * np.sign(Q_cos_theta + eps), Q_cos_theta)
830
+
831
+ dyQ_dF = (-1 / safe_denom**2) * Mo * dQ_dF * cos_theta
832
+ dyQ_dtheta = (-1 / safe_denom**2) * Mo * (dQ_dtheta * cos_theta - Q * sin_theta)
821
833
 
822
834
  # First-order partial derivatives of R1 (Equations 35-36)
823
835
  dR1_dF = np.sum(dQ_dF)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: xslope
3
- Version: 0.1.11
3
+ Version: 0.1.13
4
4
  Summary: Slope stability analysis (limit equilibrium and FEM) in Python.
5
5
  Author: Norman L. Jones
6
6
  Project-URL: Homepage, https://github.com/njones61/xslope
@@ -0,0 +1,21 @@
1
+ xslope/__init__.py,sha256=ZygAIkX6Nbjag1czWdQa-yP-GM1mBE_9ss21Xh__JFc,34
2
+ xslope/_version.py,sha256=XLyb183vl7CU3KttGJB7AeLEWBwd9veAWre8ysdY9ww,51
3
+ xslope/advanced.py,sha256=-eL4-I36j6DfsheaGbOtclUHTFkkSn1k5k4YF9tOCvU,18278
4
+ xslope/fem.py,sha256=x3BWHqxqJd-K78NzLquqr8eLXWVdFMzyA2cQXTNqLTk,115719
5
+ xslope/fileio.py,sha256=BajiNljdwMMF2u2SqLpObZ-rimhgm3XyaFo14N6vZJI,38180
6
+ xslope/global_config.py,sha256=Cj8mbPidIuj5Ty-5cZM-c8H12kNvyHsk5_ofNGez-3M,2253
7
+ xslope/mesh copy.py,sha256=qtMH1yKFgHM4kNuIrxco7tKV86R3Dbf7Nok5j6MtlLY,131013
8
+ xslope/mesh.py,sha256=Kn2Um1wz50EA4xd5xpxgxxegxWOlpL3Brks0_8JLWFQ,139382
9
+ xslope/plot.py,sha256=VkItm3kASiNL3hha-W6PbjdJ7w2_yi2_4LcjH7GqokM,86265
10
+ xslope/plot_fem.py,sha256=z2FLPbIx6yNIKYcgC-LcZz2jfx0WLYFL3xJgNvQ1t-c,69488
11
+ xslope/plot_seep.py,sha256=0s8R76c_34sUwA-L-_71Kt5BZSaIFbs-xV56M69XRU4,32960
12
+ xslope/search.py,sha256=dvgKn8JCobuvyD7fClF5lcbeHESCvV8gZ_U_lQnYRok,16867
13
+ xslope/seep.py,sha256=AlZvp1kSPBfbNkpSZUNXqaefIRuT6BZPxhadmb5DFsk,93270
14
+ xslope/slice.py,sha256=5zUl3qSF4PMt2d9y6jAX881aTcUWVMeVN87wt-BbIao,46380
15
+ xslope/solve.py,sha256=_whlUuSsDqqe0wtgLowucc9dXmC8Q6J9zbViTQOmo-I,49337
16
+ xslope-0.1.13.dist-info/LICENSE,sha256=NU5J88FUai_4Ixu5DqOQukA-gcXAyX8pXLhIg8OB3AM,10969
17
+ xslope-0.1.13.dist-info/METADATA,sha256=gnmFMyTdek5oNE5MDHkb_WX6hiMKBJ9dP2DdG4OhHYI,2028
18
+ xslope-0.1.13.dist-info/NOTICE,sha256=E-sbN0MWwvJC27Z-2_G4VUHIx4IsfvLDTmOstvY4-OQ,530
19
+ xslope-0.1.13.dist-info/WHEEL,sha256=beeZ86-EfXScwlR_HKu4SllMC9wUEj_8Z_4FJ3egI2w,91
20
+ xslope-0.1.13.dist-info/top_level.txt,sha256=5qHbWJ1R2pdTNIainFyrVtFk8R1tRcwIn0kzTuwuV1Q,7
21
+ xslope-0.1.13.dist-info/RECORD,,
@@ -1,21 +0,0 @@
1
- xslope/__init__.py,sha256=ZygAIkX6Nbjag1czWdQa-yP-GM1mBE_9ss21Xh__JFc,34
2
- xslope/_version.py,sha256=EuS8ourwvf_O1n08GOG1eXlKDKt7dguYQ663MaapOxQ,51
3
- xslope/advanced.py,sha256=35k_ekQJ-mQJ1wHnoIHMc5TVfYn-2EPCJyHz827Cpbc,18281
4
- xslope/fem.py,sha256=K4_Pq06HFBpRkN3JwtXF2zw_AMnCkXvj0ggrlRFMQQw,115731
5
- xslope/fileio.py,sha256=DnFUYmJedjKXOuVPZUfTRxGfTjiIz8KyHkRDK7ddQg0,28840
6
- xslope/global_config.py,sha256=Cj8mbPidIuj5Ty-5cZM-c8H12kNvyHsk5_ofNGez-3M,2253
7
- xslope/mesh copy.py,sha256=qtMH1yKFgHM4kNuIrxco7tKV86R3Dbf7Nok5j6MtlLY,131013
8
- xslope/mesh.py,sha256=qtMH1yKFgHM4kNuIrxco7tKV86R3Dbf7Nok5j6MtlLY,131013
9
- xslope/plot.py,sha256=MexmrZJ1CVSnG0ZN3lNz67DyQdDTdO6hLlbTTBYz4Zw,56016
10
- xslope/plot_fem.py,sha256=WiFFt9cUIRwX1uCiJrPaMtmhz5LfhZnzNVJwhp0fMP8,71902
11
- xslope/plot_seep.py,sha256=ep9spY2DiMvOJELccBAj77p9HTnBRgoyqQ_brQphWHw,35166
12
- xslope/search.py,sha256=dvgKn8JCobuvyD7fClF5lcbeHESCvV8gZ_U_lQnYRok,16867
13
- xslope/seep.py,sha256=tLvme-eL0R5SGGTH2Y6z4Rrfe36UuSWWLlL_1kglee8,93030
14
- xslope/slice.py,sha256=QHawTk7XPLziHoN_ZS0jrEPYKlhPT62caUc_q-_EGjs,45236
15
- xslope/solve.py,sha256=u_sBjtsj1EeZtgSEmo80tEWoRrrMgvRjmi6P0ODO1lU,48645
16
- xslope-0.1.11.dist-info/LICENSE,sha256=NU5J88FUai_4Ixu5DqOQukA-gcXAyX8pXLhIg8OB3AM,10969
17
- xslope-0.1.11.dist-info/METADATA,sha256=ZbDxeZ2g25EwvsZDdbQIhwRB5eOu-LSpeFYmcFn0nUk,2028
18
- xslope-0.1.11.dist-info/NOTICE,sha256=E-sbN0MWwvJC27Z-2_G4VUHIx4IsfvLDTmOstvY4-OQ,530
19
- xslope-0.1.11.dist-info/WHEEL,sha256=beeZ86-EfXScwlR_HKu4SllMC9wUEj_8Z_4FJ3egI2w,91
20
- xslope-0.1.11.dist-info/top_level.txt,sha256=5qHbWJ1R2pdTNIainFyrVtFk8R1tRcwIn0kzTuwuV1Q,7
21
- xslope-0.1.11.dist-info/RECORD,,