"""planestress post-processor plotting functions."""
from __future__ import annotations
import contextlib
from collections.abc import Generator
from typing import TYPE_CHECKING, Any
import matplotlib.pyplot as plt
import numpy as np
import shapely
from matplotlib.patches import Polygon
if TYPE_CHECKING:
import matplotlib.axes
import matplotlib.figure
import planestress.pre.boundary_condition as bc
[docs]
@contextlib.contextmanager
def plotting_context(
ax: matplotlib.axes.Axes | None = None,
pause: bool = True,
title: str = "",
filename: str = "",
render: bool = True,
axis_index: int | tuple[int, int] | None = None,
**kwargs: Any,
) -> Generator[
tuple[matplotlib.figure.Figure, matplotlib.axes.Axes | Any | None], None, None
]:
"""Executes code required to set up a matplotlib figure.
Args:
ax: Axes object on which to plot. Defaults to ``None``.
pause: If set to ``True``, the figure pauses the script until the window is
closed. If set to ``False``, the script continues immediately after the
window is rendered. Defaults to ``True``.
title: Plot title. Defaults to ``""``.
filename: Pass a non-empty string or path to save the image. If this option is
used, the figure is closed after the file is saved. Defaults to ``""``.
render: If set to ``False``, the image is not displayed. This may be useful if
the figure or axes will be embedded or further edited before being
displayed. Defaults to ``True``.
axis_index: If more than 1 axis is created by subplot, then this is the axis to
plot on. This may be a tuple if a 2D array of plots is returned. The default
value of ``None`` will select the top left plot.
kwargs: Passed to :func:`matplotlib.pyplot.subplots`
Raises:
ValueError: ``axis_index`` is invalid
Yields:
Matplotlib figure and axes
"""
if filename:
render = False
if ax is None:
if not render or pause:
plt.ioff()
else:
plt.ion()
ax_supplied = False
fig, ax = plt.subplots(**kwargs)
try:
if axis_index is None:
axis_index = (0,) * ax.ndim # type: ignore
ax = ax[axis_index] # type: ignore
except (AttributeError, TypeError):
pass # only 1 axis, not an array
except IndexError as exc:
raise ValueError(
f"axis_index={axis_index} is not compatible with arguments to "
f"subplots: {kwargs}."
) from exc
else:
fig = ax.get_figure() # type: ignore
assert fig
ax_supplied = True
if not render:
plt.ioff()
yield fig, ax
if ax is not None:
ax.set_title(title)
plt.tight_layout()
ax.set_aspect("equal", anchor="C")
# if no axes was supplied, finish the plot and return the figure and axes
if ax_supplied:
# if an axis was supplied, don't continue displaying or configuring the plot
return
if filename:
fig.savefig(filename, dpi=fig.dpi)
plt.close(fig) # close the figure to free the memory
return # if the figure was to be saved, then don't show it also
if render:
if pause:
plt.show() # type: ignore
else:
plt.draw()
plt.pause(0.001)
[docs]
def plot_boundary_conditions(
ax: matplotlib.axes.Axes,
node_loads: list[bc.NodeLoad],
line_loads: list[bc.LineLoad],
node_supports: list[bc.NodeSupport],
line_supports: list[bc.LineSupport],
node_springs: list[bc.NodeSpring],
line_springs: list[bc.LineSpring],
max_dim: float,
bc_text: bool,
bc_fmt: str,
arrow_length_scale: float,
arrow_width_scale: float,
support_scale: float,
num_supports: int,
multi_polygon: shapely.MultiPolygon,
) -> None:
"""Plots the boundary conditions.
Args:
ax: Axis to plot on.
node_loads: List of ``NodeLoad`` objects.
line_loads: List of ``LineLoad`` objects.
node_supports: List of ``NodeSupport`` objects.
line_supports: List of ``LineSupport`` objects.
node_springs: List of ``NodeSpring`` objects.
line_springs: List of ``LineSpring`` objects.
max_dim: Maximum dimension of the geometry bounding box.
bc_text: If set to ``True``, plots the values of the boundary conditions.
bc_fmt: Boundary condition text formatting string.
arrow_length_scale: Arrow length scaling factor.
arrow_width_scale: Arrow width scaling factor.
support_scale: Support scaling factor.
num_supports: Number of line supports to plot internally.
multi_polygon: ``MultiPolygon`` describing the geometry.
"""
# plot node loads
plot_node_loads(
ax=ax,
node_loads=node_loads,
max_dim=max_dim,
bc_text=bc_text,
bc_fmt=bc_fmt,
arrow_length_scale=arrow_length_scale,
arrow_width_scale=arrow_width_scale,
multi_polygon=multi_polygon,
)
# plot line loads
plot_line_loads(
ax=ax,
line_loads=line_loads,
max_dim=max_dim,
bc_text=bc_text,
bc_fmt=bc_fmt,
arrow_length_scale=arrow_length_scale,
arrow_width_scale=arrow_width_scale,
multi_polygon=multi_polygon,
)
# plot node supports
plot_node_supports(
ax=ax,
node_supports=node_supports,
max_dim=max_dim,
bc_text=bc_text,
bc_fmt=bc_fmt,
arrow_length_scale=arrow_length_scale,
arrow_width_scale=arrow_width_scale,
support_scale=support_scale,
multi_polygon=multi_polygon,
)
# plot line supports
plot_line_supports(
ax=ax,
line_supports=line_supports,
max_dim=max_dim,
bc_text=bc_text,
bc_fmt=bc_fmt,
arrow_length_scale=arrow_length_scale,
arrow_width_scale=arrow_width_scale,
support_scale=support_scale,
num_supports=num_supports,
multi_polygon=multi_polygon,
)
# plot node springs
plot_node_springs(
ax=ax,
node_springs=node_springs,
max_dim=max_dim,
support_scale=support_scale,
)
# plot line springs
plot_line_springs(
ax=ax,
line_springs=line_springs,
max_dim=max_dim,
support_scale=support_scale,
num_supports=num_supports,
)
[docs]
def plot_node_loads(
ax: matplotlib.axes.Axes,
node_loads: list[bc.NodeLoad],
max_dim: float,
bc_text: bool,
bc_fmt: str,
arrow_length_scale: float,
arrow_width_scale: float,
multi_polygon: shapely.MultiPolygon,
) -> None:
"""Plots the nodal loads.
Args:
ax: Axis to plot on.
node_loads: List of ``NodeLoad`` objects.
max_dim: Maximum dimension of the geometry bounding box.
bc_text: If set to ``True``, plots the values of the boundary conditions.
bc_fmt: Boundary condition text formatting string.
arrow_length_scale: Arrow length scaling factor.
arrow_width_scale: Arrow width scaling factor.
multi_polygon: ``MultiPolygon`` describing the geometry.
"""
# max arrow length and width
max_arrow_length = arrow_length_scale * max_dim
min_arrow_length = 0.2 * max_arrow_length
width = arrow_width_scale * max_dim
# determine maximum load
max_load = 0.0
for node_load in node_loads:
max_load = max(max_load, abs(node_load.value))
# plot each load
for node_load in node_loads:
# get length of the arrow
arrow_length = max(
abs(node_load.value) / max_load * max_arrow_length, min_arrow_length
)
arrow_length = node_load.value / abs(node_load.value) * arrow_length # signed
# calculate position and translations
x = node_load.point[0]
y = node_load.point[1]
dx = arrow_length if node_load.direction in ["x", "xy"] else 0.0
dy = arrow_length if node_load.direction in ["y", "xy"] else 0.0
# check to see if arrow tip is in geometry or on boundary
pt = shapely.Point(x + dx, y + dy)
if multi_polygon.contains(pt) or multi_polygon.boundary.contains(pt):
# push arrow outside
x -= dx
y -= dy
outside = True
else:
outside = False
# plot load
ax.arrow(
x=x, y=y, dx=dx, dy=dy, width=width, length_includes_head=True, color="k"
)
# ensure text is in right place
if outside:
dx = 0
dy = 0
# plot load text
if bc_text:
ax.annotate(
text=f"{node_load.value:>{bc_fmt}}", xy=(x + dx, y + dy), color="k"
)
[docs]
def plot_line_loads(
ax: matplotlib.axes.Axes,
line_loads: list[bc.LineLoad],
max_dim: float,
bc_text: bool,
bc_fmt: str,
arrow_length_scale: float,
arrow_width_scale: float,
multi_polygon: shapely.MultiPolygon,
) -> None:
"""Plots the line loads.
Args:
ax: Axis to plot on.
line_loads: List of ``LineLoad`` objects.
max_dim: Maximum dimension of the geometry bounding box.
bc_text: If set to ``True``, plots the values of the boundary conditions.
bc_fmt: Boundary condition text formatting string.
arrow_length_scale: Arrow length scaling factor.
arrow_width_scale: Arrow width scaling factor.
multi_polygon: ``MultiPolygon`` describing the geometry.
"""
# max arrow length and width
max_arrow_length = arrow_length_scale * max_dim
min_arrow_length = 0.2 * max_arrow_length
width = arrow_width_scale * max_dim
# determine maximum load
max_load = 0.0
for line_load in line_loads:
max_load = max(max_load, abs(line_load.value))
# plot each load
for line_load in line_loads:
# get length of the arrow
arrow_length = max(
abs(line_load.value) / max_load * max_arrow_length, min_arrow_length
)
arrow_length = line_load.value / abs(line_load.value) * arrow_length # signed
# calculate position and translations
x1 = line_load.point1[0]
y1 = line_load.point1[1]
x2 = line_load.point2[0]
y2 = line_load.point2[1]
dx = arrow_length if line_load.direction in ["x", "xy"] else 0.0
dy = arrow_length if line_load.direction in ["y", "xy"] else 0.0
# check to see if arrow tip is in geometry or on boundary
pt = shapely.Point(0.5 * (x1 + x2) + dx, 0.5 * (y1 + y2) + dy)
if multi_polygon.contains(pt) or multi_polygon.boundary.contains(pt):
# push arrow outside
x1 -= dx
y1 -= dy
x2 -= dx
y2 -= dy
outside = True
else:
outside = False
# plot line load
ax.arrow(
x=x1, y=y1, dx=dx, dy=dy, width=width, length_includes_head=True, color="k"
)
ax.arrow(
x=x2, y=y2, dx=dx, dy=dy, width=width, length_includes_head=True, color="k"
)
# ensure line and text are in right place if arrow is pushed outside
if outside:
dx = 0
dy = 0
# plot line
ax.plot([x1 + dx, x2 + dx], [y1 + dy, y2 + dy], color="k")
# plot load text
if bc_text:
ax.annotate(
text=f"{line_load.value:>{bc_fmt}}",
xy=(0.5 * (x1 + x2) + dx, 0.5 * (y1 + y2) + dy),
color="k",
)
[docs]
def plot_node_supports(
ax: matplotlib.axes.Axes,
node_supports: list[bc.NodeSupport],
max_dim: float,
bc_text: bool,
bc_fmt: str,
arrow_length_scale: float,
arrow_width_scale: float,
support_scale: float,
multi_polygon: shapely.MultiPolygon,
) -> None:
"""Plots the nodal supports.
Args:
ax: Axis to plot on.
node_supports: List of ``NodeSupport`` objects.
max_dim: Maximum dimension of the geometry bounding box.
bc_text: If set to ``True``, plots the values of the boundary conditions.
bc_fmt: Boundary condition text formatting string.
arrow_length_scale: Arrow length scaling factor.
arrow_width_scale: Arrow width scaling factor.
support_scale: Support scaling factor.
multi_polygon: ``MultiPolygon`` describing the geometry.
"""
# split into fixed supports and imposed displacements and get max displacement
node_displacements: list[bc.NodeSupport] = []
node_fixed_supports: list[bc.NodeSupport] = []
max_disp = 0.0
for node_support in node_supports:
if abs(node_support.value) > 0:
node_displacements.append(node_support)
max_disp = max(max_disp, abs(node_support.value))
else:
node_fixed_supports.append(node_support)
# plot imposed displacements
# max arrow length and width
max_arrow_length = arrow_length_scale * max_dim
min_arrow_length = 0.2 * max_arrow_length
width = arrow_width_scale * max_dim
for node_disp in node_displacements:
# get length of the arrow
arrow_length = max(
abs(node_disp.value) / max_disp * max_arrow_length, min_arrow_length
)
arrow_length = node_disp.value / abs(node_disp.value) * arrow_length # signed
# calculate position and translations
x = node_disp.point[0]
y = node_disp.point[1]
dx = arrow_length if node_disp.direction in ["x", "xy"] else 0.0
dy = arrow_length if node_disp.direction in ["y", "xy"] else 0.0
# check to see if arrow tip is in geometry or on boundary
pt = shapely.Point(x + dx, y + dy)
if multi_polygon.contains(pt) or multi_polygon.boundary.contains(pt):
# push arrow outside
x -= dx
y -= dy
outside = True
else:
outside = False
# plot load
ax.arrow(
x=x,
y=y,
dx=dx,
dy=dy,
width=width,
length_includes_head=True,
color="k",
linestyle="--",
fill=False,
)
# ensure text is in right place
if outside:
dx = 0
dy = 0
# plot load text
if bc_text:
ax.annotate(
text=f"{node_disp.value:>{bc_fmt}}", xy=(x + dx, y + dy), color="k"
)
# plot supports
# triangle coordinates
dx = support_scale * max_dim # scaling factor
h = np.sqrt(3) / 2
triangle = np.array([[-h, -h, -h, 0, -h], [-1, 1, 0.5, 0, -0.5]]) * dx
for node_fix in node_fixed_supports:
# calculate position
x = node_fix.point[0]
y = node_fix.point[1]
# determine rotation
if node_fix.direction == "x":
angle = 0.0
elif node_fix.direction == "y":
angle = np.pi / 2
else:
angle = np.pi / 2
# rotation matrix
s = np.sin(angle)
c = np.cos(angle)
rot_mat = np.array([[c, -s], [s, c]])
# plot rollers or pin extras
if node_fix.direction in ["x", "y"]:
line = np.array([[-1.1, -1.1], [-1, 1]]) * dx
rot_line = rot_mat @ line
ax.plot(rot_line[0, :] + x, rot_line[1, :] + y, "k-", linewidth=1)
else:
rect = np.array([[-1.4, -1.4, -h, -h], [-1, 1, 1, -1]]) * dx
rot_rect = rot_mat @ rect
rot_rect[0, :] += x
rot_rect[1, :] += y
ax.add_patch(Polygon(rot_rect.transpose(), facecolor=(0.7, 0.7, 0.7)))
# rotate triangle
rot_triangle = rot_mat @ triangle
# plot triangle
ax.plot(rot_triangle[0, :] + x, rot_triangle[1, :] + y, "k-", linewidth=1)
[docs]
def plot_line_supports(
ax: matplotlib.axes.Axes,
line_supports: list[bc.LineSupport],
max_dim: float,
bc_text: bool,
bc_fmt: str,
arrow_length_scale: float,
arrow_width_scale: float,
support_scale: float,
num_supports: int,
multi_polygon: shapely.MultiPolygon,
) -> None:
"""Plots the line supports.
Args:
ax: Axis to plot on.
line_supports: List of ``LineSupport`` objects.
max_dim: Maximum dimension of the geometry bounding box.
bc_text: If set to ``True``, plots the values of the boundary conditions.
bc_fmt: Boundary condition text formatting string.
arrow_length_scale: Arrow length scaling factor.
arrow_width_scale: Arrow width scaling factor.
support_scale: Support scaling factor.
num_supports: Number of line supports to plot internally.
multi_polygon: ``MultiPolygon`` describing the geometry.
"""
# split into fixed supports and imposed displacements and get max displacement
line_displacements: list[bc.LineSupport] = []
line_fixed_supports: list[bc.LineSupport] = []
max_disp = 0.0
for line_support in line_supports:
if abs(line_support.value) > 0:
line_displacements.append(line_support)
max_disp = max(max_disp, abs(line_support.value))
else:
line_fixed_supports.append(line_support)
# plot imposed displacements
# max arrow length and width
max_arrow_length = arrow_length_scale * max_dim
min_arrow_length = 0.2 * max_arrow_length
width = arrow_width_scale * max_dim
for line_disp in line_displacements:
# get length of the arrow
arrow_length = max(
abs(line_disp.value) / max_disp * max_arrow_length, min_arrow_length
)
arrow_length = line_disp.value / abs(line_disp.value) * arrow_length # signed
# calculate position and translations
x1 = line_disp.point1[0]
y1 = line_disp.point1[1]
x2 = line_disp.point2[0]
y2 = line_disp.point2[1]
dx = arrow_length if line_disp.direction in ["x", "xy"] else 0.0
dy = arrow_length if line_disp.direction in ["y", "xy"] else 0.0
# check to see if arrow tip is in geometry or on boundary
pt = shapely.Point(0.5 * (x1 + x2) + dx, 0.5 * (y1 + y2) + dy)
if multi_polygon.contains(pt) or multi_polygon.boundary.contains(pt):
# push arrow outside
x1 -= dx
y1 -= dy
x2 -= dx
y2 -= dy
outside = True
else:
outside = False
# plot load
ax.arrow(
x=x1,
y=y1,
dx=dx,
dy=dy,
width=width,
length_includes_head=True,
color="k",
linestyle="--",
fill=False,
)
ax.arrow(
x=x2,
y=y2,
dx=dx,
dy=dy,
width=width,
length_includes_head=True,
color="k",
linestyle="--",
fill=False,
)
# ensure line and text are in right place if arrow is pushed outside
if outside:
dx = 0
dy = 0
# plot line
ax.plot([x1 + dx, x2 + dx], [y1 + dy, y2 + dy], color="k", linestyle="--")
# plot load text
if bc_text:
ax.annotate(
text=f"{line_disp.value:>{bc_fmt}}",
xy=(0.5 * (x1 + x2) + dx, 0.5 * (y1 + y2) + dy),
color="k",
)
# plot supports
# triangle coordinates
dx = support_scale * max_dim # scaling factor
h = np.sqrt(3) / 2
triangle = np.array([[-h, -h, -h, 0, -h], [-1, 1, 0.5, 0, -0.5]]) * dx
for line_fix in line_fixed_supports:
# determine rotation
if line_fix.direction == "x":
angle = 0.0
elif line_fix.direction == "y":
angle = np.pi / 2
else:
angle = np.pi / 2
# rotation matrix
s = np.sin(angle)
c = np.cos(angle)
rot_mat = np.array([[c, -s], [s, c]])
# rotate triangle
rot_triangle = rot_mat @ triangle
# plot supports
num_plots = num_supports + 2
x_len = line_fix.point2[0] - line_fix.point1[0]
y_len = line_fix.point2[1] - line_fix.point1[1]
for idx in range(num_plots):
# calculate position
x = line_fix.point1[0] + idx / (num_plots - 1) * x_len
y = line_fix.point1[1] + idx / (num_plots - 1) * y_len
# plot triangle
ax.plot(rot_triangle[0, :] + x, rot_triangle[1, :] + y, "k-", linewidth=1)
# plot rollers or pin extras
if line_fix.direction in ["x", "y"]:
line = np.array([[-1.1, -1.1], [-1, 1]]) * dx
rot_line = rot_mat @ line
ax.plot(rot_line[0, :] + x, rot_line[1, :] + y, "k-", linewidth=1)
else:
rect = np.array([[-1.4, -1.4, -h, -h], [-1, 1, 1, -1]]) * dx
rot_rect = rot_mat @ rect
rot_rect[0, :] += x
rot_rect[1, :] += y
ax.add_patch(Polygon(rot_rect.transpose(), facecolor=(0.7, 0.7, 0.7)))
[docs]
def plot_node_springs(
ax: matplotlib.axes.Axes,
node_springs: list[bc.NodeSpring],
max_dim: float,
support_scale: float,
) -> None:
"""Plots the nodal supports.
Args:
ax: Axis to plot on.
node_springs: List of ``NodeSpring`` objects.
max_dim: Maximum dimension of the geometry bounding box.
support_scale: Support scaling factor.
"""
# triangle coordinates
dx = support_scale * max_dim # scaling factor
h = np.sqrt(3) / 2
hs = h / 8
triangle = (
np.array([[-2 * h, -2 * h, -2 * h, -h, -2 * h], [-1, 1, 0.5, 0, -0.5]]) * dx
)
spring = (
np.array(
[
[
-8 * hs,
-7 * hs,
-6 * hs,
-5 * hs,
-4 * hs,
-3 * hs,
-2 * hs,
-1 * hs,
0,
],
[0, 0, -0.25, 0.25, -0.25, 0.25, -0.25, 0, 0],
]
)
* dx
)
for node_spring in node_springs:
# calculate position
x = node_spring.point[0]
y = node_spring.point[1]
# determine rotation
if node_spring.direction == "x":
angles = [0.0]
elif node_spring.direction == "y":
angles = [np.pi / 2]
else:
angles = [0.0, np.pi / 2]
for angle in angles:
# rotation matrix
s = np.sin(angle)
c = np.cos(angle)
rot_mat = np.array([[c, -s], [s, c]])
# plot rollers
line = np.array([[-1.1 - h, -1.1 - h], [-1, 1]]) * dx
rot_line = rot_mat @ line
ax.plot(rot_line[0, :] + x, rot_line[1, :] + y, "k-", linewidth=1)
# rotate triangle and spring
rot_triangle = rot_mat @ triangle
rot_spring = rot_mat @ spring
# plot triangle and spring
ax.plot(rot_triangle[0, :] + x, rot_triangle[1, :] + y, "k-", linewidth=1)
ax.plot(rot_spring[0, :] + x, rot_spring[1, :] + y, "k-", linewidth=1)
[docs]
def plot_line_springs(
ax: matplotlib.axes.Axes,
line_springs: list[bc.LineSpring],
max_dim: float,
support_scale: float,
num_supports: int,
) -> None:
"""Plots the nodal supports.
Args:
ax: Axis to plot on.
line_springs: List of ``LineSpring`` objects.
max_dim: Maximum dimension of the geometry bounding box.
support_scale: Support scaling factor.
num_supports: Number of line supports to plot internally.
"""
# triangle coordinates
dx = support_scale * max_dim # scaling factor
h = np.sqrt(3) / 2
hs = h / 8
triangle = (
np.array([[-2 * h, -2 * h, -2 * h, -h, -2 * h], [-1, 1, 0.5, 0, -0.5]]) * dx
)
spring = (
np.array(
[
[
-8 * hs,
-7 * hs,
-6 * hs,
-5 * hs,
-4 * hs,
-3 * hs,
-2 * hs,
-1 * hs,
0,
],
[0, 0, -0.25, 0.25, -0.25, 0.25, -0.25, 0, 0],
]
)
* dx
)
for line_spring in line_springs:
# determine rotation
if line_spring.direction == "x":
angles = [0.0]
elif line_spring.direction == "y":
angles = [np.pi / 2]
else:
angles = [0.0, np.pi / 2]
for angle in angles:
# rotation matrix
s = np.sin(angle)
c = np.cos(angle)
rot_mat = np.array([[c, -s], [s, c]])
# rotate triangle and spring
rot_triangle = rot_mat @ triangle
rot_spring = rot_mat @ spring
# plot supports
num_plots = num_supports + 2
x_len = line_spring.point2[0] - line_spring.point1[0]
y_len = line_spring.point2[1] - line_spring.point1[1]
for idx in range(num_plots):
# calculate position
x = line_spring.point1[0] + idx / (num_plots - 1) * x_len
y = line_spring.point1[1] + idx / (num_plots - 1) * y_len
# plot rollers
line = np.array([[-1.1 - h, -1.1 - h], [-1, 1]]) * dx
rot_line = rot_mat @ line
ax.plot(rot_line[0, :] + x, rot_line[1, :] + y, "k-", linewidth=1)
# plot triangle and spring
ax.plot(
rot_triangle[0, :] + x, rot_triangle[1, :] + y, "k-", linewidth=1
)
ax.plot(rot_spring[0, :] + x, rot_spring[1, :] + y, "k-", linewidth=1)