API Reference - Plot FEM
plot_deformed_mesh(ax, fem_data, solution, deform_scale=1.0, show_mesh=True, show_reinforcement=True, cbar_shrink=0.8, cbar_labelpad=20, label_elements=False)
Plot deformed mesh overlay on original mesh.
Shows the original mesh in light gray and the deformed mesh in blue. Uses VP displacement (total - elastic) when available to show the failure mechanism rather than gravity settlement. The deform_scale parameter amplifies the displacements for visibility.
Source code in xslope/plot_fem.py
def plot_deformed_mesh(ax, fem_data, solution, deform_scale=1.0, show_mesh=True, show_reinforcement=True,
cbar_shrink=0.8, cbar_labelpad=20, label_elements=False):
"""
Plot deformed mesh overlay on original mesh.
Shows the original mesh in light gray and the deformed mesh in blue.
Uses VP displacement (total - elastic) when available to show the failure
mechanism rather than gravity settlement. The deform_scale parameter
amplifies the displacements for visibility.
"""
nodes = fem_data["nodes"]
elements = fem_data["elements"]
element_types = fem_data["element_types"]
displacements = solution.get("displacements", np.zeros(2 * len(nodes)))
# Use VP displacement (total - elastic) if available, to show failure mechanism
disp_elastic = solution.get("displacements_elastic", None)
if disp_elastic is not None:
disp = displacements - disp_elastic
else:
disp = displacements
# Calculate deformed node positions
u, v = _extract_uv(disp, fem_data)
nodes_deformed = nodes + deform_scale * np.column_stack([u, v])
# Plot original mesh
if show_mesh:
plot_mesh_lines(ax, fem_data, color='lightgray', alpha=0.5, linewidth=1.0, label='Original')
# Plot deformed mesh
fem_data_deformed = fem_data.copy()
fem_data_deformed["nodes"] = nodes_deformed
plot_mesh_lines(ax, fem_data_deformed, color='blue', alpha=0.8, linewidth=1.5, label='Deformed')
# Plot reinforcement in both original and deformed configurations
if show_reinforcement and 'elements_1d' in fem_data:
plot_reinforcement_lines(ax, fem_data, solution, color='gray', alpha=0.5, linewidth=2, label='Original Reinforcement')
plot_reinforcement_lines(ax, fem_data_deformed, solution, color='red', alpha=0.8, linewidth=2, label='Deformed Reinforcement')
# Add element labels if requested
if label_elements:
_add_element_labels(ax, fem_data_deformed) # Label on deformed mesh
# Add a dummy colorbar to maintain consistent spacing with other plots
# This ensures the x-axis alignment is consistent across all subplots
dummy_data = np.array([[0, 1]])
dummy_im = ax.imshow(dummy_data, cmap='viridis', alpha=0)
cbar = plt.colorbar(dummy_im, ax=ax, shrink=cbar_shrink)
cbar.set_label('Deformation Scale', rotation=270, labelpad=cbar_labelpad, color='white')
cbar.set_ticks([]) # Remove tick marks
cbar.set_ticklabels([]) # Remove tick labels
# Make the colorbar completely invisible by setting colors to background
cbar.outline.set_color('white') # Make the border invisible
cbar.outline.set_linewidth(0) # Remove the border line
# Note: Axis limits will be set by the calling function for consistent multi-plot alignment
# When used as a standalone plot, matplotlib will auto-scale appropriately
F = solution.get("F", None)
disp_label = 'Viscoplastic Deformation' if disp_elastic is not None else 'Mesh Deformation'
scale_str = f'{deform_scale:.0f}' if deform_scale >= 10 else f'{deform_scale:.1f}'
title = f'{disp_label} (Scale = {scale_str}x)'
if F is not None:
title += f' F={F:.2f}'
ax.set_title(title, fontsize=12, pad=15)
ax.set_xlabel('x')
ax.set_ylabel('y')
plot_displacement_contours(ax, fem_data, solution, show_mesh=True, show_reinforcement=True, cbar_shrink=0.8, cbar_labelpad=20, label_elements=False)
Plot total displacement magnitude as filled contours using the viridis colormap.
Source code in xslope/plot_fem.py
def plot_displacement_contours(ax, fem_data, solution, show_mesh=True, show_reinforcement=True,
cbar_shrink=0.8, cbar_labelpad=20, label_elements=False):
"""
Plot total displacement magnitude as filled contours using the viridis colormap.
"""
nodes = fem_data["nodes"]
elements = fem_data["elements"]
element_types = fem_data["element_types"]
displacements = solution.get("displacements", np.zeros(2 * len(nodes)))
# Calculate displacement magnitudes
u, v = _extract_uv(displacements, fem_data)
disp_mag = np.sqrt(u**2 + v**2)
# Create triangulation for contouring
triangles = []
for i, elem in enumerate(elements):
elem_type = element_types[i]
if elem_type == 3: # Triangle
triangles.append([elem[0], elem[1], elem[2]])
elif elem_type == 4: # Quad - split into triangles
triangles.append([elem[0], elem[1], elem[2]])
triangles.append([elem[0], elem[2], elem[3]])
elif elem_type == 6: # 6-node triangle - use corner nodes
triangles.append([elem[0], elem[1], elem[2]])
elif elem_type in [8, 9]: # 8-node or 9-node quad - use corner nodes
triangles.append([elem[0], elem[1], elem[2]])
triangles.append([elem[0], elem[2], elem[3]])
if triangles:
triangles = np.array(triangles)
# Create contour plot
tcf = ax.tricontourf(nodes[:, 0], nodes[:, 1], triangles, disp_mag,
levels=20, cmap='viridis', alpha=0.8)
# Colorbar
cbar = plt.colorbar(tcf, ax=ax, shrink=cbar_shrink)
cbar.set_label('Displacement Magnitude', rotation=270, labelpad=cbar_labelpad)
# Plot mesh
if show_mesh:
plot_mesh_lines(ax, fem_data, color='black', alpha=0.3, linewidth=0.5)
# Plot reinforcement
if show_reinforcement and 'elements_1d' in fem_data:
plot_reinforcement_lines(ax, fem_data, solution)
# Add element labels if requested
if label_elements:
_add_element_labels(ax, fem_data)
ax.set_aspect('equal')
ax.set_title('Displacement Magnitude Contours')
ax.set_xlabel('x')
ax.set_ylabel('y')
plot_displacement_vectors(ax, fem_data, solution, show_mesh=True, show_reinforcement=True, cbar_shrink=0.8, cbar_labelpad=20, label_elements=False, plot_nodes=False, plot_elements=False, plot_boundary=True, displacement_tolerance=1e-06, scale_vectors=True)
Plot displacement vectors at corner nodes of each element.
If viscoplastic solution data is available (displacements_elastic in solution), plots VP displacement (total - elastic) to show the failure mechanism rather than the gravity settlement. Otherwise plots total displacement.
| Parameters: |
|
|---|
Source code in xslope/plot_fem.py
def plot_displacement_vectors(ax, fem_data, solution, show_mesh=True, show_reinforcement=True,
cbar_shrink=0.8, cbar_labelpad=20, label_elements=False,
plot_nodes=False, plot_elements=False, plot_boundary=True,
displacement_tolerance=1e-6, scale_vectors=True):
"""
Plot displacement vectors at corner nodes of each element.
If viscoplastic solution data is available (displacements_elastic in solution),
plots VP displacement (total - elastic) to show the failure mechanism rather
than the gravity settlement. Otherwise plots total displacement.
Parameters:
ax: Matplotlib axes
fem_data: FEM data dictionary
solution: FEM solution dictionary
show_mesh: Show mesh lines or boundary
show_reinforcement: Show reinforcement elements
label_elements: Show element ID labels
plot_nodes: If True, show dots at all node locations
plot_elements: If True, show all element edges
plot_boundary: If True, show only boundary edges (default)
displacement_tolerance: Fraction of max displacement below which vectors are hidden
scale_vectors: If True (default), auto-scale vectors for visibility
"""
nodes = fem_data["nodes"]
elements = fem_data["elements"]
element_types = fem_data["element_types"]
displacements = solution.get("displacements", np.zeros(2 * len(nodes)))
# Use VP displacement (total - elastic) if available, to show failure mechanism
# This removes the gravity settlement and shows only plastic deformation
disp_elastic = solution.get("displacements_elastic", None)
if disp_elastic is not None:
disp_vp = displacements - disp_elastic
u, v = _extract_uv(disp_vp, fem_data)
else:
u, v = _extract_uv(displacements, fem_data)
disp_mag = np.sqrt(u**2 + v**2)
max_disp_mag = np.max(disp_mag)
if max_disp_mag < 1e-30:
print("Warning: No VP displacements to plot")
return
# Collect corner nodes only (avoid mid-side nodes of quad8/tri6)
corner_nodes = set()
for i, elem in enumerate(elements):
et = element_types[i]
if et == 8 or et == 9:
for j in range(4):
corner_nodes.add(elem[j])
elif et == 6:
for j in range(3):
corner_nodes.add(elem[j])
else:
for j in range(et):
corner_nodes.add(elem[j])
# Absolute threshold
abs_tol = displacement_tolerance * max_disp_mag
# Plot boundary outline first
if show_mesh:
if plot_elements:
plot_mesh_lines(ax, fem_data, color='lightgray', alpha=0.5, linewidth=0.5)
elif plot_boundary:
boundary_edges = _get_mesh_boundary(fem_data)
for edge in boundary_edges:
x_coords = [nodes[edge[0], 0], nodes[edge[1], 0]]
y_coords = [nodes[edge[0], 1], nodes[edge[1], 1]]
ax.plot(x_coords, y_coords, 'k-', alpha=0.7, linewidth=1.0)
# Plot small vectors at corner nodes
corner_list = sorted(corner_nodes)
cx = nodes[corner_list, 0]
cy = nodes[corner_list, 1]
cu = u[corner_list]
cv = v[corner_list]
cmag = disp_mag[corner_list]
mask = cmag > abs_tol
if np.sum(mask) == 0:
print("Warning: All displacements below tolerance")
return
ax.quiver(cx[mask], cy[mask], cu[mask], cv[mask],
angles='xy', color='black', alpha=0.7,
scale=None, width=0.002, headwidth=3, headlength=4,
headaxislength=3, pivot='tail')
# Plot node dots if requested
if plot_nodes:
ax.plot(nodes[:, 0], nodes[:, 1], 'k.', markersize=1, alpha=0.4)
# Plot reinforcement
if show_reinforcement and 'elements_1d' in fem_data:
plot_reinforcement_lines(ax, fem_data, solution)
# Add element labels if requested
if label_elements:
_add_element_labels(ax, fem_data)
# Dummy colorbar for axis alignment with other subplots
dummy_data = np.array([[0, 1]])
dummy_im = ax.imshow(dummy_data, cmap='viridis', alpha=0)
cbar = plt.colorbar(dummy_im, ax=ax, shrink=cbar_shrink)
cbar.set_label('', color='white')
cbar.set_ticks([])
cbar.set_ticklabels([])
cbar.outline.set_color('white')
cbar.outline.set_linewidth(0)
F = solution.get("F", None)
title = 'Viscoplastic Displacement Vectors' if disp_elastic is not None else 'Displacement Vectors'
if F is not None:
title += f' F={F:.2f}'
ax.set_title(title, fontsize=12, pad=15)
plot_fem_data(fem_data, figsize=(14, 6), show_nodes=False, show_bc=True, label_elements=False, label_nodes=False, alpha=0.4, bc_symbol_size=0.03, save_png=False, dpi=300)
Plots a FEM mesh colored by material zone with boundary conditions displayed.
| Parameters: |
|
|---|
Source code in xslope/plot_fem.py
def plot_fem_data(fem_data, figsize=(14, 6), show_nodes=False, show_bc=True,
label_elements=False, label_nodes=False, alpha=0.4, bc_symbol_size=0.03, save_png=False, dpi=300):
"""
Plots a FEM mesh colored by material zone with boundary conditions displayed.
Args:
fem_data: Dictionary containing FEM data from build_fem_data
figsize: Figure size
show_nodes: If True, plot node points
show_bc: If True, plot boundary condition symbols
label_elements: If True, label each element with its number at its centroid
label_nodes: If True, label each node with its number just above and to the right
alpha: Transparency for element faces
bc_symbol_size: Size factor for boundary condition symbols (as fraction of mesh size)
"""
from matplotlib.collections import PatchCollection
# Extract data from fem_data
nodes = fem_data["nodes"]
elements = fem_data["elements"]
element_materials = fem_data["element_materials"]
element_types = fem_data.get("element_types", None)
bc_type = fem_data["bc_type"]
bc_values = fem_data["bc_values"]
fig, ax = plt.subplots(figsize=figsize)
materials = np.unique(element_materials)
# Import get_material_color to ensure consistent colors with plot_mesh
from .plot import get_material_color
mat_to_color = {mat: get_material_color(mat) for mat in materials}
# If element_types is not provided, assume all triangles (backward compatibility)
if element_types is None:
element_types = np.full(len(elements), 3)
# Batch polygons and edge lines by material for efficient rendering
# Key: material -> list of polygon vertex arrays
mat_fill_polys = {mat: [] for mat in materials}
edge_segments = [] # for outer boundaries of quadratic elements
for idx, element_nodes in enumerate(elements):
element_type = element_types[idx]
mat = element_materials[idx]
if element_type == 3: # Linear triangle
mat_fill_polys[mat].append(nodes[element_nodes[:3]])
elif element_type == 6: # Quadratic triangle - subdivide into 4 sub-triangles
n0, n1, n2 = nodes[element_nodes[0]], nodes[element_nodes[1]], nodes[element_nodes[2]]
n3, n4, n5 = nodes[element_nodes[3]], nodes[element_nodes[4]], nodes[element_nodes[5]]
mat_fill_polys[mat].extend([
np.array([n0, n3, n5]),
np.array([n3, n1, n4]),
np.array([n5, n4, n2]),
np.array([n3, n4, n5]),
])
# Outer boundary edges
edge_segments.extend([[n0, n1], [n1, n2], [n2, n0]])
elif element_type == 4: # Linear quadrilateral
mat_fill_polys[mat].append(nodes[element_nodes[:4]])
elif element_type == 8: # Quadratic quadrilateral - subdivide into 4 sub-quads
n0, n1, n2, n3 = nodes[element_nodes[0]], nodes[element_nodes[1]], nodes[element_nodes[2]], nodes[element_nodes[3]]
n4, n5, n6, n7 = nodes[element_nodes[4]], nodes[element_nodes[5]], nodes[element_nodes[6]], nodes[element_nodes[7]]
center = np.array([(n0[0]+n1[0]+n2[0]+n3[0]+n4[0]+n5[0]+n6[0]+n7[0]) / 8,
(n0[1]+n1[1]+n2[1]+n3[1]+n4[1]+n5[1]+n6[1]+n7[1]) / 8])
mat_fill_polys[mat].extend([
np.array([n0, n4, center, n7]),
np.array([n4, n1, n5, center]),
np.array([center, n5, n2, n6]),
np.array([n7, center, n6, n3]),
])
edge_segments.extend([[n0, n1], [n1, n2], [n2, n3], [n3, n0]])
elif element_type == 9: # 9-node quadrilateral
n0, n1, n2, n3 = nodes[element_nodes[0]], nodes[element_nodes[1]], nodes[element_nodes[2]], nodes[element_nodes[3]]
n4, n5, n6, n7 = nodes[element_nodes[4]], nodes[element_nodes[5]], nodes[element_nodes[6]], nodes[element_nodes[7]]
center = nodes[element_nodes[8]]
mat_fill_polys[mat].extend([
np.array([n0, n4, center, n7]),
np.array([n4, n1, n5, center]),
np.array([center, n5, n2, n6]),
np.array([n7, center, n6, n3]),
])
edge_segments.extend([[n0, n1], [n1, n2], [n2, n3], [n3, n0]])
# Render filled polygons as batched PatchCollections (one per material)
for mat in materials:
polys = mat_fill_polys[mat]
if not polys:
continue
has_edge = any(element_types[i] in (3, 4) for i, m in enumerate(element_materials) if m == mat)
has_no_edge = any(element_types[i] in (6, 8, 9) for i, m in enumerate(element_materials) if m == mat)
color = mat_to_color[mat]
if has_edge and not has_no_edge:
# All linear elements — draw with edges
patch_list = [Polygon(p) for p in polys]
pc = PatchCollection(patch_list, facecolor=color, edgecolor='k', linewidth=0.5, alpha=alpha)
ax.add_collection(pc)
elif has_no_edge and not has_edge:
# All quadratic sub-polys — no edges on fills
patch_list = [Polygon(p) for p in polys]
pc = PatchCollection(patch_list, facecolor=color, edgecolor='none', alpha=alpha)
ax.add_collection(pc)
else:
# Mixed — separate linear (with edges) and quadratic sub-polys (no edges)
linear_polys = []
sub_polys = []
sub_idx = 0
for i in range(len(elements)):
if element_materials[i] != mat:
continue
et = element_types[i]
if et == 3:
linear_polys.append(polys[sub_idx]); sub_idx += 1
elif et == 4:
linear_polys.append(polys[sub_idx]); sub_idx += 1
elif et == 6:
sub_polys.extend(polys[sub_idx:sub_idx+4]); sub_idx += 4
elif et in (8, 9):
sub_polys.extend(polys[sub_idx:sub_idx+4]); sub_idx += 4
if linear_polys:
pc = PatchCollection([Polygon(p) for p in linear_polys], facecolor=color, edgecolor='k', linewidth=0.5, alpha=alpha)
ax.add_collection(pc)
if sub_polys:
pc = PatchCollection([Polygon(p) for p in sub_polys], facecolor=color, edgecolor='none', alpha=alpha)
ax.add_collection(pc)
# Render outer boundary edges of quadratic elements as a single LineCollection
if edge_segments:
lc = LineCollection(edge_segments, colors='k', linewidths=0.5)
ax.add_collection(lc)
# Label element numbers at centroids if requested
if label_elements:
for idx, element_nodes in enumerate(elements):
element_type = element_types[idx]
if element_type == 3:
element_coords = nodes[element_nodes[:3]]
elif element_type == 4:
element_coords = nodes[element_nodes[:4]]
elif element_type == 6:
element_coords = nodes[element_nodes[:6]]
elif element_type == 8:
element_coords = nodes[element_nodes[:8]]
else:
element_coords = nodes[element_nodes[:9]]
centroid = np.mean(element_coords, axis=0)
ax.text(centroid[0], centroid[1], str(idx+1),
ha='center', va='center', fontsize=6, color='black', alpha=0.4,
zorder=10)
if show_nodes:
ax.plot(nodes[:, 0], nodes[:, 1], 'k.', markersize=2)
# Label node numbers if requested
if label_nodes:
for i, (x, y) in enumerate(nodes):
ax.text(x + 0.5, y + 0.5, str(i+1), fontsize=6, color='blue', alpha=0.7,
ha='left', va='bottom', zorder=11)
# Get material names if available
material_names = fem_data.get("material_names", [])
legend_handles = []
for mat in materials:
if material_names and mat <= len(material_names):
label = material_names[mat - 1]
else:
label = f"Material {mat}"
legend_handles.append(
plt.Line2D([0], [0], color=mat_to_color[mat], lw=4, label=label)
)
# Plot 1D elements (reinforcement truss + pile beam) using LineCollection
elements_1d = fem_data.get("elements_1d", np.array([]).reshape(0, 3))
pile_elem_mask = fem_data.get("pile_elem_mask", np.zeros(len(elements_1d), dtype=bool))
n_reinf_plotted = 0
n_pile_plotted = 0
if len(elements_1d) > 0:
reinf_segs = []
pile_segs = []
for elem_idx in range(len(elements_1d)):
elem_nodes_1d = elements_1d[elem_idx]
seg = [nodes[elem_nodes_1d[0]], nodes[elem_nodes_1d[1]]]
if pile_elem_mask[elem_idx]:
pile_segs.append(seg)
n_pile_plotted += 1
else:
reinf_segs.append(seg)
n_reinf_plotted += 1
if reinf_segs:
lc = LineCollection(reinf_segs, colors='red', linewidths=2.5, zorder=5)
ax.add_collection(lc)
legend_handles.append(
plt.Line2D([0], [0], color='red', lw=2.5, label=f'Reinforcement ({n_reinf_plotted} elements)')
)
if pile_segs:
lc = LineCollection(pile_segs, colors='green', linewidths=3.5, zorder=5)
ax.add_collection(lc)
legend_handles.append(
plt.Line2D([0], [0], color='green', lw=3.5, label=f'Pile ({n_pile_plotted} elements)')
)
# Plot boundary conditions
if show_bc:
saved_roller_x = fem_data.get("roller_x_nodes", set())
_plot_boundary_conditions(ax, nodes, bc_type, bc_values, legend_handles, bc_symbol_size, saved_roller_x)
# Single combined legend outside the plot
ax.legend(
handles=legend_handles,
loc='upper center',
bbox_to_anchor=(0.5, -0.1),
ncol=3, # or more, depending on how many items you have
frameon=False
)
# Adjust plot limits to accommodate force arrows
x_min, x_max = nodes[:, 0].min(), nodes[:, 0].max()
y_min, y_max = nodes[:, 1].min(), nodes[:, 1].max()
# Add extra space for force arrows if they exist
force_nodes = np.where(bc_type == 4)[0]
if len(force_nodes) > 0:
# Find the extent of force arrows
mesh_size = min(x_max - x_min, y_max - y_min)
symbol_size = mesh_size * bc_symbol_size
# Add padding for force arrows (they extend outward from nodes)
y_padding = symbol_size * 4 # Extra space above for upward arrows
x_padding = (x_max - x_min) * 0.05 # Standard padding
y_padding_bottom = (y_max - y_min) * 0.05
else:
# Standard padding
x_padding = (x_max - x_min) * 0.05
y_padding = (y_max - y_min) * 0.05
y_padding_bottom = y_padding
ax.set_xlim(x_min - x_padding, x_max + x_padding)
ax.set_ylim(y_min - y_padding_bottom, y_max + y_padding)
ax.set_aspect("equal")
# Count element types for title
num_tri = np.sum((element_types == 3) | (element_types == 6))
num_quad = np.sum((element_types == 4) | (element_types == 8) | (element_types == 9))
num_1d = len(elements_1d)
parts = []
if num_tri > 0:
parts.append(f"{num_tri} triangles")
if num_quad > 0:
parts.append(f"{num_quad} quads")
if n_reinf_plotted > 0:
parts.append(f"{n_reinf_plotted} reinforcement")
if n_pile_plotted > 0:
parts.append(f"{n_pile_plotted} pile")
title = f"FEM Mesh with Material Zones ({', '.join(parts)})"
ax.set_title(title)
plt.tight_layout()
if save_png:
filename = 'plot_' + title.lower().replace(' ', '_').replace(':', '').replace(',', '').replace('(', '').replace(')', '') + '.png'
plt.savefig(filename, dpi=dpi, bbox_inches='tight')
plt.show()
plot_fem_results(fem_data, solution, plot_type=['deformation', 'shear_strain', 'displace_vector'], deform_percent=15, show_mesh=True, show_reinforcement=True, figsize=(12, 8), label_elements=False, plot_nodes=False, plot_elements=False, plot_boundary=True, displacement_tolerance=0.5, scale_vectors=False, save_png=False, dpi=300)
Plot FEM results with various visualization options.
| Parameters: |
|
|---|
Source code in xslope/plot_fem.py
def plot_fem_results(fem_data, solution, plot_type=['deformation', 'shear_strain', 'displace_vector'],
deform_percent=15, show_mesh=True, show_reinforcement=True, figsize=(12, 8), label_elements=False,
plot_nodes=False, plot_elements=False, plot_boundary=True, displacement_tolerance=0.5,
scale_vectors=False, save_png=False, dpi=300):
"""
Plot FEM results with various visualization options.
Parameters:
fem_data: FEM data dictionary from build_fem_data
solution: FEM solution dictionary from solve_fem
plot_type: Comma-separated plot types. Valid types:
'deformation' - deformed mesh overlay
'displace_mag' - displacement magnitude contours
'displace_vector' - displacement vectors at corner nodes
'stress' - von Mises stress contours
'strain' - equivalent strain contours
'shear_strain' - viscoplastic max shear strain contours
'yield' - Mohr-Coulomb yield function contours
deform_percent: Target deformation as percentage of mesh height (default 15).
show_mesh: Show mesh lines
show_reinforcement: Show reinforcement elements
figsize: Figure size (width, height)
label_elements: Show element ID labels at centroids
plot_nodes: For displace_vector, show dots at node locations
plot_elements: For displace_vector, show all element edges
plot_boundary: For displace_vector, show boundary edges only (default)
displacement_tolerance: Fraction of max displacement below which vectors are hidden
scale_vectors: For displace_vector, auto-scale vectors for visibility
save_png: Save figure to PNG file
dpi: Resolution for saved PNG
"""
nodes = fem_data["nodes"]
elements = fem_data["elements"]
element_types = fem_data["element_types"]
displacements = solution.get("displacements", np.zeros(2 * len(nodes)))
# Accept a single string or a list of strings
if isinstance(plot_type, str):
plot_types = [plot_type.strip().lower()]
else:
plot_types = [pt.strip().lower() for pt in plot_type]
valid_types = ['displace_mag', 'displace_vector', 'deformation', 'stress', 'strain', 'shear_strain', 'yield']
# Validate plot types
for pt in plot_types:
if pt not in valid_types:
raise ValueError(f"Unknown plot_type: '{pt}'. Valid types: {valid_types}")
# Auto-calculate deformation scale so max displacement is deform_percent of mesh height
# Use VP displacement if available (matches what plot_deformed_mesh will plot)
disp_elastic = solution.get("displacements_elastic", None)
disp_for_scale = displacements - disp_elastic if disp_elastic is not None else displacements
u_arr, v_arr = _extract_uv(disp_for_scale, fem_data)
max_disp = np.max(np.sqrt(u_arr**2 + v_arr**2))
mesh_height = np.max(nodes[:, 1]) - np.min(nodes[:, 1])
if max_disp > 1e-30:
deform_scale = max(1.0, (mesh_height * deform_percent / 100) / max_disp)
else:
deform_scale = 1.0
# Create subplots based on number of plot types
# When the first plot is 'deformation', add a thin extra row for its legend
n_plots = len(plot_types)
has_deform_legend = n_plots > 1 and plot_types[0] == 'deformation'
if n_plots == 1:
fig, ax = plt.subplots(figsize=figsize, layout='constrained')
axes = [ax]
legend_ax = None
else:
height_factor = min(0.8, 1.2 / n_plots)
total_height = figsize[1] * n_plots * height_factor
if has_deform_legend:
# Add a thin row after the first plot for the legend
height_ratios = [1] + [0.08] + [1] * (n_plots - 1)
fig, all_axes = plt.subplots(n_plots + 1, 1,
figsize=(figsize[0], total_height),
layout='constrained',
gridspec_kw={'height_ratios': height_ratios})
axes = [all_axes[0]] + list(all_axes[2:])
legend_ax = all_axes[1]
legend_ax.set_axis_off()
else:
fig, axes = plt.subplots(n_plots, 1,
figsize=(figsize[0], total_height),
layout='constrained')
legend_ax = None
if not isinstance(axes, (list, np.ndarray)):
axes = [axes]
elif isinstance(axes, np.ndarray):
axes = list(axes)
# Calculate overall mesh bounds for consistent axis limits
nodes = fem_data["nodes"]
x_min, x_max = np.min(nodes[:, 0]), np.max(nodes[:, 0])
y_min, y_max = np.min(nodes[:, 1]), np.max(nodes[:, 1])
# Add small margin
x_margin = (x_max - x_min) * 0.05
y_margin = (y_max - y_min) * 0.05
# Plot each type
for i, pt in enumerate(plot_types):
ax = axes[i]
# Calculate colorbar parameters based on number of plots
if n_plots == 1:
cbar_shrink = 0.8
cbar_labelpad = 20
elif n_plots == 2:
cbar_shrink = 0.7 # Slightly larger than before
cbar_labelpad = 15
else: # 3 or more plots
cbar_shrink = 0.5 # Slightly larger than before
cbar_labelpad = 12
if pt == 'displace_mag':
plot_displacement_contours(ax, fem_data, solution, show_mesh, show_reinforcement,
cbar_shrink=cbar_shrink, cbar_labelpad=cbar_labelpad, label_elements=label_elements)
elif pt == 'displace_vector':
plot_displacement_vectors(ax, fem_data, solution, show_mesh, show_reinforcement,
cbar_shrink=cbar_shrink, cbar_labelpad=cbar_labelpad, label_elements=label_elements,
plot_nodes=plot_nodes, plot_elements=plot_elements, plot_boundary=plot_boundary,
displacement_tolerance=displacement_tolerance, scale_vectors=scale_vectors)
elif pt == 'deformation':
plot_deformed_mesh(ax, fem_data, solution, deform_scale, show_mesh, show_reinforcement,
cbar_shrink=cbar_shrink, cbar_labelpad=cbar_labelpad, label_elements=label_elements)
elif pt == 'stress':
plot_stress_contours(ax, fem_data, solution, show_mesh, show_reinforcement,
cbar_shrink=cbar_shrink, cbar_labelpad=cbar_labelpad, label_elements=label_elements)
elif pt == 'strain':
plot_strain_contours(ax, fem_data, solution, show_mesh, show_reinforcement,
cbar_shrink=cbar_shrink, cbar_labelpad=cbar_labelpad, label_elements=label_elements)
elif pt == 'shear_strain':
plot_shear_strain_contours(ax, fem_data, solution, show_mesh, show_reinforcement,
cbar_shrink=cbar_shrink, cbar_labelpad=cbar_labelpad, label_elements=label_elements)
elif pt == 'yield':
plot_yield_function_contours(ax, fem_data, solution, show_mesh, show_reinforcement,
cbar_shrink=cbar_shrink, cbar_labelpad=cbar_labelpad, label_elements=label_elements)
# Set consistent axis limits for all plots (including single plots)
ax.set_xlim(x_min - x_margin, x_max + x_margin)
ax.set_ylim(y_min - y_margin, y_max + y_margin)
ax.set_aspect('equal')
# Place deformation legend in the dedicated legend row
if has_deform_legend and legend_ax is not None:
handles, labels = axes[0].get_legend_handles_labels()
if handles:
legend_ax.legend(handles, labels, loc='center', ncol=4, fontsize=10,
frameon=False)
if save_png:
filename = 'fem_results.png'
plt.savefig(filename, dpi=dpi, bbox_inches='tight')
plt.show()
# Return appropriate values
if n_plots == 1:
return fig, axes[0]
else:
return fig, axes
plot_mesh_lines(ax, fem_data, color='black', alpha=1.0, linewidth=1.0, label=None)
Plot mesh element boundaries.
Source code in xslope/plot_fem.py
def plot_mesh_lines(ax, fem_data, color='black', alpha=1.0, linewidth=1.0, label=None):
"""
Plot mesh element boundaries.
"""
nodes = fem_data["nodes"]
elements = fem_data["elements"]
element_types = fem_data["element_types"]
lines = []
for i, elem in enumerate(elements):
elem_type = element_types[i]
if elem_type == 3: # Triangle
# Add triangle edges
edges = [(elem[0], elem[1]), (elem[1], elem[2]), (elem[2], elem[0])]
elif elem_type == 4: # Quadrilateral
# Add quad edges
edges = [(elem[0], elem[1]), (elem[1], elem[2]), (elem[2], elem[3]), (elem[3], elem[0])]
elif elem_type == 6: # 6-node triangle - use corner nodes
edges = [(elem[0], elem[1]), (elem[1], elem[2]), (elem[2], elem[0])]
elif elem_type in [8, 9]: # 8-node or 9-node quad - use corner nodes
edges = [(elem[0], elem[1]), (elem[1], elem[2]), (elem[2], elem[3]), (elem[3], elem[0])]
else:
continue
for edge in edges:
line_coords = nodes[[edge[0], edge[1]]]
lines.append(line_coords)
if lines:
lc = LineCollection(lines, colors=color, alpha=alpha, linewidths=linewidth, label=label)
ax.add_collection(lc)
plot_reinforcement_force_profiles(fem_data, solution, figsize=(12, 8), save_png=False, dpi=300)
Plot axial force profiles along each reinforcement line as subplots.
Source code in xslope/plot_fem.py
def plot_reinforcement_force_profiles(fem_data, solution, figsize=(12, 8), save_png=False, dpi=300):
"""
Plot axial force profiles along each reinforcement line as subplots.
"""
if 'elements_1d' not in fem_data:
print("No reinforcement elements found")
return None, None
nodes = fem_data["nodes"]
elements_1d = fem_data["elements_1d"]
element_materials_1d = fem_data["element_materials_1d"]
forces_1d = solution.get("forces_1d", np.zeros(len(elements_1d)))
t_allow = fem_data.get("t_allow_by_1d_elem", np.ones(len(elements_1d)))
t_res = fem_data.get("t_res_by_1d_elem", np.zeros(len(elements_1d)))
failed_1d = solution.get("failed_1d_elements", np.zeros(len(elements_1d), dtype=bool))
# Group elements by reinforcement line (material ID)
unique_lines = np.unique(element_materials_1d)
n_lines = len(unique_lines)
if n_lines == 0:
print("No reinforcement lines found")
return None, None
# Create subplot layout
if n_lines <= 3:
fig, axes = plt.subplots(n_lines, 1, figsize=figsize, squeeze=False)
axes = axes.flatten()
else:
rows = int(np.ceil(n_lines / 2))
fig, axes = plt.subplots(rows, 2, figsize=figsize, squeeze=False)
axes = axes.flatten()
for line_idx, line_id in enumerate(unique_lines):
ax = axes[line_idx]
# Get elements for this line
line_elements = np.where(element_materials_1d == line_id)[0]
if len(line_elements) == 0:
continue
# Get element positions along the line
positions = []
forces = []
t_allow_line = []
t_res_line = []
failed_line = []
for elem_idx in line_elements:
elem = elements_1d[elem_idx]
# Use midpoint of element
mid_point = 0.5 * (nodes[elem[0]] + nodes[elem[1]])
# Distance along line (simplified - use x-coordinate)
positions.append(mid_point[0])
forces.append(forces_1d[elem_idx])
t_allow_line.append(t_allow[elem_idx])
t_res_line.append(t_res[elem_idx])
failed_line.append(failed_1d[elem_idx])
# Sort by position
sorted_indices = np.argsort(positions)
positions = np.array(positions)[sorted_indices]
forces = np.array(forces)[sorted_indices]
t_allow_line = np.array(t_allow_line)[sorted_indices]
t_res_line = np.array(t_res_line)[sorted_indices]
failed_line = np.array(failed_line)[sorted_indices]
# Plot force profile
ax.plot(positions, forces, 'b-o', linewidth=2, markersize=6, label='Tensile Force')
ax.plot(positions, t_allow_line, 'g--', linewidth=1, label='Allowable Force')
if np.any(t_res_line > 0):
ax.plot(positions, t_res_line, 'orange', linestyle='--', linewidth=1, label='Residual Force')
# Mark failed elements
if np.any(failed_line):
failed_positions = positions[failed_line]
failed_forces = forces[failed_line]
ax.scatter(failed_positions, failed_forces, color='red', s=100, marker='x',
linewidth=3, label='Failed Elements', zorder=10)
# Formatting
ax.set_xlabel('Position along line')
ax.set_ylabel('Force')
ax.set_title(f'Reinforcement Line {line_id} Force Profile')
ax.grid(True, alpha=0.3)
ax.legend()
# Set y-limits to show all relevant values
max_val = max(np.max(np.abs(forces)), np.max(t_allow_line))
if max_val > 0:
ax.set_ylim([-max_val * 0.1, max_val * 1.1])
# Hide unused subplots
for i in range(n_lines, len(axes)):
axes[i].set_visible(False)
plt.tight_layout()
if save_png:
filename = 'plot_reinforcement_force_profiles.png'
plt.savefig(filename, dpi=dpi, bbox_inches='tight')
return fig, axes
plot_reinforcement_forces(ax, fem_data, solution)
Plot reinforcement elements colored by force level.
Color scheme: - Blue to green to yellow to red: 0 to Tmax (tension force ramp) - Magenta: element has yielded and is at residual capacity Tres - White/open with dashed outline: element has pulled out (broken, T=0) - Gray: element carrying no tension (inactive or in compression)
Source code in xslope/plot_fem.py
def plot_reinforcement_forces(ax, fem_data, solution):
"""
Plot reinforcement elements colored by force level.
Color scheme:
- Blue to green to yellow to red: 0 to Tmax (tension force ramp)
- Magenta: element has yielded and is at residual capacity Tres
- White/open with dashed outline: element has pulled out (broken, T=0)
- Gray: element carrying no tension (inactive or in compression)
"""
if 'elements_1d' not in fem_data:
return
from matplotlib.colors import LinearSegmentedColormap
import matplotlib.cm as cm
nodes = fem_data["nodes"]
elements_1d = fem_data["elements_1d"]
forces_1d = solution.get("forces_1d", np.zeros(len(elements_1d)))
t_allow = fem_data.get("t_allow_by_1d_elem", np.ones(len(elements_1d)))
t_res = fem_data.get("t_res_by_1d_elem", np.zeros(len(elements_1d)))
failed_1d = solution.get("failed_1d_elements", np.zeros(len(elements_1d), dtype=bool))
# Find global Tmax (max of all t_allow values)
t_max_global = t_allow.max() if len(t_allow) > 0 else 1.0
# Custom colormap: blue -> white -> red (coolwarm style)
force_cmap = LinearSegmentedColormap.from_list(
'force_ramp', ['#2166ac', '#f7f7f7', '#d73027'], N=256)
# Classify and draw each element
normal_lines = []
normal_colors = []
tres_lines = []
pullout_lines = []
inactive_lines = []
pile_elem_mask = fem_data.get("pile_elem_mask", np.zeros(len(elements_1d), dtype=bool))
pile_force_lines = []
pile_force_colors = []
forces_pile_lateral = solution.get("forces_pile_lateral", np.array([]))
# Build pile element index mapping: global 1d index -> pile force index
pile_force_idx = 0
for i in range(len(elements_1d)):
elem = elements_1d[i]
coords = nodes[elem[:2]]
if pile_elem_mask[i]:
# Pile element — color by lateral (shear) force
if pile_force_idx < len(forces_pile_lateral):
pile_force_lines.append(coords)
pile_force_colors.append(abs(forces_pile_lateral[pile_force_idx]))
pile_force_idx += 1
continue
force = forces_1d[i]
is_failed = failed_1d[i]
if is_failed and t_res[i] < 1e-6 and force < 1e-6:
pullout_lines.append(coords)
elif is_failed and t_res[i] > 1e-6:
tres_lines.append(coords)
elif force > 1e-6:
ratio = min(force / t_max_global, 1.0) if t_max_global > 0 else 0.0
normal_lines.append(coords)
normal_colors.append(force_cmap(ratio))
else:
inactive_lines.append(coords)
# Draw inactive elements (cyan, solid)
if inactive_lines:
lc_outline = LineCollection(inactive_lines, colors='black', linewidths=4.5, alpha=0.9, zorder=3.9)
ax.add_collection(lc_outline)
lc = LineCollection(inactive_lines, colors='#00CC00', linewidths=3, alpha=0.9, zorder=4)
ax.add_collection(lc)
ax.plot([], [], '-', color='#00CC00', linewidth=3, alpha=0.9, label='Inactive (no tension)')
# Draw normal tension elements (force-colored)
if normal_lines:
lc_outline = LineCollection(normal_lines, colors='black', linewidths=4.5, alpha=0.9, zorder=4.9)
ax.add_collection(lc_outline)
lc = LineCollection(normal_lines, colors=normal_colors, linewidths=3, alpha=0.9, zorder=5)
ax.add_collection(lc)
# Add colorbar
sm = cm.ScalarMappable(cmap=force_cmap, norm=plt.Normalize(0, t_max_global))
sm.set_array([])
cbar = plt.colorbar(sm, ax=ax, shrink=0.6, pad=0.02)
cbar.set_label('Reinforcement Force', rotation=270, labelpad=15, fontsize=10)
# Draw elements at Tres (magenta)
if tres_lines:
lc_outline = LineCollection(tres_lines, colors='black', linewidths=4.5, alpha=0.9, zorder=5.9)
ax.add_collection(lc_outline)
lc = LineCollection(tres_lines, colors='magenta', linewidths=3, alpha=0.9, zorder=6)
ax.add_collection(lc)
ax.plot([], [], '-', color='magenta', linewidth=3, label='At residual (Tres)')
# Draw pulled-out elements (orange, solid)
if pullout_lines:
lc_outline = LineCollection(pullout_lines, colors='black', linewidths=4.5, alpha=0.9, zorder=5.9)
ax.add_collection(lc_outline)
lc = LineCollection(pullout_lines, colors='black', linewidths=3, alpha=0.9, zorder=6)
ax.add_collection(lc)
ax.plot([], [], '-', color='black', linewidth=3, alpha=0.9, label='Pulled out')
# Draw pile elements colored by lateral (shear) force
if pile_force_lines:
from matplotlib.colors import Normalize
max_lateral = max(pile_force_colors) if pile_force_colors else 1.0
pile_cmap = plt.cm.Greens
pile_norm = Normalize(vmin=0, vmax=max_lateral if max_lateral > 0 else 1.0)
colors = [pile_cmap(pile_norm(v)) for v in pile_force_colors]
lc_outline = LineCollection(pile_force_lines, colors='black', linewidths=5, alpha=0.9, zorder=5.9)
ax.add_collection(lc_outline)
lc = LineCollection(pile_force_lines, colors=colors, linewidths=3.5, alpha=0.9, zorder=6)
ax.add_collection(lc)
sm = cm.ScalarMappable(cmap=pile_cmap, norm=pile_norm)
sm.set_array([])
cbar = plt.colorbar(sm, ax=ax, shrink=0.6, pad=0.02)
cbar.set_label('Pile Shear Force', rotation=270, labelpad=15, fontsize=10)
# Add legend if any special states exist
handles, labels = ax.get_legend_handles_labels()
if handles:
ax.legend(loc='lower right', fontsize=9, framealpha=0.9)
plot_reinforcement_lines(ax, fem_data, solution, color='red', alpha=1.0, linewidth=2, label=None)
Plot reinforcement and pile elements as lines with distinct colors.
Source code in xslope/plot_fem.py
def plot_reinforcement_lines(ax, fem_data, solution, color='red', alpha=1.0, linewidth=2, label=None):
"""
Plot reinforcement and pile elements as lines with distinct colors.
"""
if 'elements_1d' not in fem_data:
return
nodes = fem_data["nodes"]
elements_1d = fem_data["elements_1d"]
element_types_1d = fem_data["element_types_1d"]
pile_elem_mask = fem_data.get("pile_elem_mask", np.zeros(len(elements_1d), dtype=bool))
reinf_lines = []
pile_lines = []
for i, elem in enumerate(elements_1d):
elem_type = element_types_1d[i]
if elem_type >= 2:
line_coords = nodes[elem[:2]]
if pile_elem_mask[i]:
pile_lines.append(line_coords)
else:
reinf_lines.append(line_coords)
if reinf_lines:
lc = LineCollection(reinf_lines, colors=color, alpha=alpha, linewidths=linewidth, label=label)
ax.add_collection(lc)
if pile_lines:
pile_label = label.replace('Reinforcement', 'Pile') if label and 'Reinforcement' in label else None
lc = LineCollection(pile_lines, colors='green', alpha=alpha, linewidths=linewidth + 1, label=pile_label)
ax.add_collection(lc)
plot_shear_strain_contours(ax, fem_data, solution, show_mesh=True, show_reinforcement=True, cbar_shrink=0.8, cbar_labelpad=20, label_elements=False)
Plot viscoplastic max shear strain contours.
Uses accumulated viscoplastic strains from the solution (vp_shear_strain key). Falls back to total shear strain if VP data is not available.
Source code in xslope/plot_fem.py
def plot_shear_strain_contours(ax, fem_data, solution, show_mesh=True, show_reinforcement=True,
cbar_shrink=0.8, cbar_labelpad=20, label_elements=False):
"""
Plot viscoplastic max shear strain contours.
Uses accumulated viscoplastic strains from the solution (vp_shear_strain key).
Falls back to total shear strain if VP data is not available.
"""
nodes = fem_data["nodes"]
elements = fem_data["elements"]
element_types = fem_data["element_types"]
vp_shear_strain = solution.get("vp_shear_strain", None)
if vp_shear_strain is None:
# Fallback to total shear strain if VP not available
strains = solution.get("strains", np.zeros((len(elements), 4)))
if strains.shape[1] >= 4:
vp_shear_strain = strains[:, 3]
else:
print("Warning: Shear strain data not available")
return
_plot_nodal_contours(ax, fem_data, vp_shear_strain, 'VP Max Shear Strain',
False, False, cbar_shrink, cbar_labelpad,
colormap='coolwarm', label_elements=label_elements)
# Draw reinforcement with force-based coloring
if show_reinforcement and 'elements_1d' in fem_data:
plot_reinforcement_forces(ax, fem_data, solution)
F = solution.get("F", None)
title = 'Viscoplastic Shear Strain'
if F is not None:
title += f' F={F:.2f}'
ax.set_title(title, fontsize=12, pad=15)
plot_ssrm_convergence(ssrm_solution, figsize=(10, 6), save_png=False, dpi=300)
Plot SSRM bisection convergence history showing F vs iteration and convergence status.
Source code in xslope/plot_fem.py
def plot_ssrm_convergence(ssrm_solution, figsize=(10, 6), save_png=False, dpi=300):
"""
Plot SSRM bisection convergence history showing F vs iteration and convergence status.
"""
if 'F_history' not in ssrm_solution:
print("No SSRM convergence history found")
return None, None
F_history = ssrm_solution['F_history']
convergence_history = ssrm_solution['convergence_history']
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=figsize)
# Plot F vs iteration
iterations = range(1, len(F_history) + 1)
colors = ['green' if conv else 'red' for conv in convergence_history]
ax1.scatter(iterations, F_history, c=colors, s=50, alpha=0.7)
ax1.plot(iterations, F_history, 'k-', alpha=0.5)
# Mark final FS
if 'FS' in ssrm_solution and ssrm_solution['FS'] is not None:
ax1.axhline(y=ssrm_solution['FS'], color='blue', linestyle='--',
linewidth=2, label=f"FS = {ssrm_solution['FS']:.3f}")
ax1.legend()
ax1.set_xlabel('SSRM Iteration')
ax1.set_ylabel('Reduction Factor F')
ax1.set_title('SSRM Convergence History')
ax1.grid(True, alpha=0.3)
# Plot convergence status
conv_status = [1 if conv else 0 for conv in convergence_history]
ax2.bar(iterations, conv_status, color=colors, alpha=0.7, width=0.8)
ax2.set_xlabel('SSRM Iteration')
ax2.set_ylabel('Converged')
ax2.set_title('Convergence Status (Green=Converged, Red=Failed)')
ax2.set_ylim([0, 1.2])
ax2.grid(True, alpha=0.3)
plt.tight_layout()
if save_png:
filename = 'plot_ssrm_convergence.png'
plt.savefig(filename, dpi=dpi, bbox_inches='tight')
return fig, (ax1, ax2)
plot_strain_contours(ax, fem_data, solution, show_mesh=True, show_reinforcement=True, cbar_shrink=0.8, cbar_labelpad=20, label_elements=False)
Plot von Mises equivalent strain contours computed from total strains.
Source code in xslope/plot_fem.py
def plot_strain_contours(ax, fem_data, solution, show_mesh=True, show_reinforcement=True,
cbar_shrink=0.8, cbar_labelpad=20, label_elements=False):
"""
Plot von Mises equivalent strain contours computed from total strains.
"""
nodes = fem_data["nodes"]
elements = fem_data["elements"]
element_types = fem_data["element_types"]
strains = solution.get("strains", np.zeros((len(elements), 4)))
if strains.shape[1] < 3:
print("Warning: Strain data not available or incomplete")
return
# Calculate equivalent strain (von Mises equivalent strain)
# For plane strain: equiv_strain = sqrt(2/3) * sqrt(eps_x^2 + eps_y^2 + eps_x*eps_y + 3/4*gamma_xy^2)
eps_x = strains[:, 0]
eps_y = strains[:, 1]
gamma_xy = strains[:, 2]
equiv_strain = np.sqrt((2/3) * (eps_x**2 + eps_y**2 + eps_x*eps_y + 0.75*gamma_xy**2))
# Plot contours
_plot_element_contours(ax, fem_data, equiv_strain, 'Equivalent Strain',
show_mesh, show_reinforcement, cbar_shrink, cbar_labelpad, label_elements)
plot_stress_contours(ax, fem_data, solution, show_mesh=True, show_reinforcement=True, cbar_shrink=0.8, cbar_labelpad=20, label_elements=False)
Plot von Mises stress contours with yielding elements highlighted.
Source code in xslope/plot_fem.py
def plot_stress_contours(ax, fem_data, solution, show_mesh=True, show_reinforcement=True,
cbar_shrink=0.8, cbar_labelpad=20, label_elements=False):
"""
Plot von Mises stress contours with yielding elements highlighted.
"""
nodes = fem_data["nodes"]
elements = fem_data["elements"]
element_types = fem_data["element_types"]
stresses = solution.get("stresses", np.zeros((len(elements), 4)))
# Use yield function to determine plastic elements for consistency
# If yield_function is available, use it; otherwise fall back to plastic_elements
yield_function = solution.get("yield_function", None)
if yield_function is not None:
plastic_elements = yield_function > 0 # F > 0 means yielding
else:
plastic_elements = solution.get("plastic_elements", np.zeros(len(elements), dtype=bool))
# Extract von Mises stresses
von_mises = stresses[:, 3] # 4th column is von Mises stress
# Create element patches with color based on stress
patches_list = []
stress_values = []
for i, elem in enumerate(elements):
elem_type = element_types[i]
if elem_type == 3: # Triangle
coords = nodes[elem[:3]]
patch = Polygon(coords, closed=True)
patches_list.append(patch)
stress_values.append(von_mises[i])
elif elem_type == 4: # Quadrilateral
coords = nodes[elem[:4]]
patch = Polygon(coords, closed=True)
patches_list.append(patch)
stress_values.append(von_mises[i])
elif elem_type == 6: # 6-node triangle - use corner nodes
coords = nodes[elem[:3]]
patch = Polygon(coords, closed=True)
patches_list.append(patch)
stress_values.append(von_mises[i])
elif elem_type in [8, 9]: # 8-node or 9-node quad - use corner nodes
coords = nodes[elem[:4]]
patch = Polygon(coords, closed=True)
patches_list.append(patch)
stress_values.append(von_mises[i])
if patches_list:
from matplotlib.collections import PatchCollection
# Create patch collection
p = PatchCollection(patches_list, alpha=0.8, edgecolors='none')
p.set_array(np.array(stress_values))
p.set_cmap('plasma')
ax.add_collection(p)
# Colorbar
cbar = plt.colorbar(p, ax=ax, shrink=cbar_shrink)
cbar.set_label('von Mises Stress', rotation=270, labelpad=cbar_labelpad)
# Highlight plastic elements with thick boundary
if np.any(plastic_elements):
for i, elem in enumerate(elements):
if plastic_elements[i]:
elem_type = element_types[i]
if elem_type == 3: # Triangle
coords = nodes[elem[:3]]
coords = np.vstack([coords, coords[0]]) # Close the polygon
ax.plot(coords[:, 0], coords[:, 1], 'r-', linewidth=2, alpha=0.8)
elif elem_type == 4: # Quadrilateral
coords = nodes[elem[:4]]
coords = np.vstack([coords, coords[0]]) # Close the polygon
ax.plot(coords[:, 0], coords[:, 1], 'r-', linewidth=2, alpha=0.8)
elif elem_type == 6: # 6-node triangle - use corner nodes
coords = nodes[elem[:3]]
coords = np.vstack([coords, coords[0]]) # Close the polygon
ax.plot(coords[:, 0], coords[:, 1], 'r-', linewidth=2, alpha=0.8)
elif elem_type in [8, 9]: # 8-node or 9-node quad - use corner nodes
coords = nodes[elem[:4]]
coords = np.vstack([coords, coords[0]]) # Close the polygon
ax.plot(coords[:, 0], coords[:, 1], 'r-', linewidth=2, alpha=0.8)
# Plot mesh
if show_mesh:
plot_mesh_lines(ax, fem_data, color='gray', alpha=0.3, linewidth=0.3)
# Plot reinforcement with force visualization
if show_reinforcement and 'elements_1d' in fem_data:
plot_reinforcement_forces(ax, fem_data, solution)
# Add element labels if requested
if label_elements:
_add_element_labels(ax, fem_data)
ax.set_aspect('equal')
ax.set_title('von Mises Stress (Red outline = Yielding/Plastic Elements)')
ax.set_xlabel('x')
ax.set_ylabel('y')
plot_yield_function_contours(ax, fem_data, solution, show_mesh=True, show_reinforcement=True, cbar_shrink=0.8, cbar_labelpad=20, label_elements=False)
Plot yield function values (Mohr-Coulomb failure criterion). Positive values indicate yielding/failure, negative values indicate elastic state.
Source code in xslope/plot_fem.py
def plot_yield_function_contours(ax, fem_data, solution, show_mesh=True, show_reinforcement=True,
cbar_shrink=0.8, cbar_labelpad=20, label_elements=False):
"""
Plot yield function values (Mohr-Coulomb failure criterion).
Positive values indicate yielding/failure, negative values indicate elastic state.
"""
nodes = fem_data["nodes"]
elements = fem_data["elements"]
element_types = fem_data["element_types"]
yield_function = solution.get("yield_function", None)
if yield_function is None:
print("Warning: Yield function data not available in solution")
# Create dummy data
yield_function = np.zeros(len(elements))
# Create custom colormap for yield function visualization
# Strong blue for very negative (very safe), white near zero, red for positive (yielding)
from matplotlib.colors import LinearSegmentedColormap
# Define color transitions for yield function
# F < 0: shades of blue/green (elastic/safe)
# F = 0: white/light gray (critical)
# F > 0: shades of red (yielding/plastic)
colors_below = ['#0000FF', '#0066FF', '#00AAFF', '#00DDDD', '#CCCCCC'] # Blue to gray
colors_above = ['#FFCCCC', '#FF9999', '#FF6666', '#FF3333', '#FF0000', '#CC0000'] # Light red to dark red
# Create custom colormap with sharp transition at F=0
n_bins = 256
n_below = int(n_bins * 0.7) # 70% for negative values
n_above = n_bins - n_below # 30% for positive values
from matplotlib.colors import ListedColormap
colors_below_interp = plt.cm.Blues_r(np.linspace(0.2, 0.9, n_below))
colors_above_interp = plt.cm.Reds(np.linspace(0.3, 1.0, n_above))
colors_all = np.vstack([colors_below_interp, colors_above_interp])
cmap_yield = ListedColormap(colors_all)
# Set visualization bounds - asymmetric to focus on near-yield region
vmin = -200 # Cap negative values for better contrast
vmax = 50 # Positive values are more important
# Plot each element as a colored patch
from matplotlib.collections import PatchCollection
from matplotlib.patches import Polygon
patches_list = []
values_list = []
for i, elem in enumerate(elements):
elem_type = element_types[i]
if elem_type == 3: # Triangle
coords = nodes[elem[:3]]
elif elem_type == 4: # Quad
coords = nodes[elem[:4]]
elif elem_type == 6: # 6-node triangle - use corner nodes
coords = nodes[elem[:3]]
elif elem_type in [8, 9]: # 8 or 9-node quad - use corner nodes
coords = nodes[elem[:4]]
else:
continue
patch = Polygon(coords, closed=True)
patches_list.append(patch)
# Clip values for visualization
values_list.append(np.clip(yield_function[i], vmin, vmax))
if patches_list:
p = PatchCollection(patches_list, alpha=0.9, edgecolors='gray', linewidths=0.3)
p.set_array(np.array(values_list))
p.set_cmap(cmap_yield)
p.set_clim(vmin, vmax)
ax.add_collection(p)
# Add colorbar with custom ticks
cbar = plt.colorbar(p, ax=ax, shrink=cbar_shrink)
cbar.set_label('Yield Function F', rotation=270, labelpad=cbar_labelpad)
# Set custom ticks to highlight key values
tick_values = [-200, -100, -50, -20, -10, -5, 0, 5, 10, 20, 50]
tick_labels = ['-200', '-100', '-50', '-20', '-10', '-5', '0', '5', '10', '20', '50']
# Filter ticks to those within bounds
valid_ticks = [(v, l) for v, l in zip(tick_values, tick_labels) if vmin <= v <= vmax]
if valid_ticks:
tick_values, tick_labels = zip(*valid_ticks)
cbar.set_ticks(tick_values)
cbar.set_ticklabels(tick_labels)
# Add a line at F=0
cbar.ax.axhline(y=0, color='black', linewidth=2)
# Add yield function values as text on elements (if requested or for yielding elements)
for i, elem in enumerate(elements):
elem_type = element_types[i]
# Get element centroid
if elem_type == 3: # Triangle
elem_nodes = nodes[elem[:3]]
elif elem_type == 4: # Quad
elem_nodes = nodes[elem[:4]]
elif elem_type == 6: # 6-node triangle - use corner nodes
elem_nodes = nodes[elem[:3]]
elif elem_type in [8, 9]: # 8 or 9-node quad - use corner nodes
elem_nodes = nodes[elem[:4]]
else:
continue
centroid = np.mean(elem_nodes, axis=0)
# Show values for elements that are close to yielding or already yielding
# or if label_elements is True
f_val = yield_function[i]
if label_elements or f_val > -50: # Show if requested or if close to yielding
# Format the number based on magnitude
if abs(f_val) < 10:
text = f'{f_val:.1f}'
else:
text = f'{f_val:.0f}'
# Choose text color based on value
if f_val > 0:
color = 'white' # White on red background
fontweight = 'bold'
elif f_val > -10:
color = 'black' # Black on light background
fontweight = 'normal'
else:
color = 'white' # White on blue background
fontweight = 'normal'
# Only show for elements near yield or if explicitly requested
if label_elements or f_val > -30:
ax.text(centroid[0], centroid[1], text,
ha='center', va='center', fontsize=5,
color=color, fontweight=fontweight, alpha=0.8)
# Highlight yielding elements with thick red border
for i, elem in enumerate(elements):
if yield_function[i] > 0:
elem_type = element_types[i]
if elem_type == 3: # Triangle
coords = nodes[elem[:3]]
elif elem_type == 4: # Quad
coords = nodes[elem[:4]]
elif elem_type == 6: # 6-node triangle - use corner nodes
coords = nodes[elem[:3]]
elif elem_type in [8, 9]: # 8 or 9-node quad - use corner nodes
coords = nodes[elem[:4]]
else:
continue
# Close the polygon
coords = np.vstack([coords, coords[0]])
ax.plot(coords[:, 0], coords[:, 1], 'k-', linewidth=2.5, alpha=1.0) # Black border for yielding elements
# Add reinforcement if requested
if show_reinforcement and 'elements_1d' in fem_data:
plot_reinforcement_lines(ax, fem_data, solution)
# Add title indicating yield state
ax.set_title('Yield Function (Red: F>0 Yielding/Plastic, Blue: F<0 Elastic)', fontsize=12, pad=15)
# Add statistics to the plot
n_yielding = np.sum(yield_function > 0)
n_total = len(yield_function)
n_critical = np.sum((yield_function > -10) & (yield_function <= 0)) # Near yielding
stats_text = f'Yielding: {n_yielding}/{n_total} elements\n'
stats_text += f'Critical (F>-10): {n_critical} elements\n'
stats_text += f'Max F: {np.max(yield_function):.1f}\n'
stats_text += f'Min F: {np.min(yield_function):.1f}'
ax.text(0.02, 0.98, stats_text,
transform=ax.transAxes, fontsize=9, verticalalignment='top',
bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.7))