Source code for anastruct.fem.plotter.mpl

import math
from typing import TYPE_CHECKING, Dict, List, Optional, Sequence, Tuple

import matplotlib.colors
import matplotlib.patches as mpatches
import matplotlib.pyplot as plt
import numpy as np

from anastruct.basic import find_nearest, rotate_xy
from anastruct.fem.plotter.values import (
    PlottingValues,
    det_scaling_factor,
    plot_values_axial_force,
    plot_values_bending_moment,
    plot_values_deflection,
    plot_values_element,
    plot_values_shear_force,
)

if TYPE_CHECKING:
    from matplotlib.axes import Axes
    from matplotlib.figure import Figure

    from anastruct.fem.node import Node
    from anastruct.fem.system import SystemElements


[docs] PATCH_SIZE = 0.03
[docs] class Plotter: def __init__(self, system: "SystemElements", mesh: int): """Class for plotting the structure. Args: system (SystemElements): System of elements mesh (int): Number of points to plot """
[docs] self.plot_values = PlottingValues(system, mesh)
[docs] self.mesh: int = self.plot_values.mesh
[docs] self.system: "SystemElements" = system
[docs] self.axes: List["Axes"] = []
[docs] self.one_fig: Optional["Axes"] = None
[docs] self.max_q: float = 0
[docs] self.max_qn: float = 0
[docs] self.max_system_point_load: float = 0
[docs] self.fig: Optional["Figure"] = None
[docs] self.plot_colors: Dict[str, str] = { "support": "r", "hinge": "k", "element": "k", "node_number": "k", "element_number": "k", "displaced_elem": "C0", "point_load_fill": "orange", "point_load_edge": "b", "point_load_text": "k", "q_load": "g", "q_load_arrow_face": "k", "q_load_arrow_edge": "k", "q_load_text": "b", "moment_load": "orange", "moment_load_text": "k", "reaction_force_arrow_edge": "orange", "reaction_force_arrow_fill": "b", "reaction_force_text": "b", "axial_force_neg": "C0", "axial_force_pos": "C1", "axial_force_sign": "b", "bending_moment": "C0", "shear_force": "C0", "annotation": "b", }
[docs] def change_plot_colors(self, colors: Dict) -> None: """Changes the plotting color for various components of the plot. Args: colors (Dict): A dictionary containing plot components and colors as key-value pairs. """ for item, color in colors.items(): if not isinstance(color, str) or not isinstance(item, str): print("Plot components and colors must be passed in as strings") elif self.plot_colors.get(item) is None: print( str(item) + " is not a valid plot component to change the color of" ) elif not matplotlib.colors.is_color_like(color): print(str(color + "is not a valid matplotlib color")) else: self.plot_colors[item] = color
@property
[docs] def max_val_structure(self) -> float: """Returns the maximum value of the structure. Returns: float: Maximum value of the structure """ return self.plot_values.max_val_structure
[docs] def __start_plot( self, figsize: Optional[Tuple[float, float]] ) -> Tuple[float, float]: """Starts the plot by initialising a matplotlib plot window of the given size. Args: figsize (Optional[Tuple[float, float]]): Figure size Returns: Tuple[float, float]: Figure size (width, height) """ plt.close("all") self.fig = plt.figure(figsize=figsize) self.axes = [self.fig.add_subplot(111)] plt.tight_layout() return (self.fig.get_figwidth(), self.fig.get_figheight())
[docs] def __fixed_support_patch(self, max_val: float, axes_i: int = 0) -> None: """Plots the fixed supports. Args: max_val (float): Max scale of the plot axes_i (int, optional): Which set of axes to plot on (for multi-plot windows). Defaults to 0. """ width = height = PATCH_SIZE * max_val for node in self.system.supports_fixed: support_patch = mpatches.Rectangle( (node.vertex.x - width * 0.5, node.vertex.y - width * 0.5), width, height, color=self.plot_colors["support"], zorder=9, ) self.axes[axes_i].add_patch(support_patch)
[docs] def __hinged_support_patch(self, max_val: float, axes_i: int = 0) -> None: """Plots the hinged supports. Args: max_val (float): Max scale of the plot axes_i (int, optional): Which set of axes to plot on (for multi-plot windows). Defaults to 0. """ radius = PATCH_SIZE * max_val for node in self.system.supports_hinged: support_patch = mpatches.RegularPolygon( (node.vertex.x, node.vertex.y - radius), numVertices=3, radius=radius, color=self.plot_colors["support"], zorder=9, ) self.axes[axes_i].add_patch(support_patch)
[docs] def __rotational_support_patch(self, max_val: float, axes_i: int = 0) -> None: """Plots the rotational supports. Args: max_val (float): Max scale of the plot axes_i (int, optional): Which set of axes to plot on (for multi-plot windows). Defaults to 0. """ width = height = PATCH_SIZE * max_val for node in self.system.supports_rotational: support_patch = mpatches.Rectangle( (node.vertex.x - width * 0.5, node.vertex.y - width * 0.5), width, height, color=self.plot_colors["support"], zorder=9, fill=False, ) self.axes[axes_i].add_patch(support_patch)
[docs] def __roll_support_patch(self, max_val: float, axes_i: int = 0) -> None: """Plots the roller supports. Args: max_val (float): Max scale of the plot axes_i (int, optional): Which set of axes to plot on (for multi-plot windows). Defaults to 0. """ radius = PATCH_SIZE * max_val count = 0 for node in self.system.supports_roll: direction = self.system.supports_roll_direction[count] rotate = self.system.supports_roll_rotate[count] x1 = np.cos(np.pi) * radius + node.vertex.x + radius # vertex.x z1 = np.sin(np.pi) * radius + node.vertex.y # vertex.y x2 = ( np.cos(np.radians(90)) * radius + node.vertex.x + radius ) # vertex.x + radius z2 = np.sin(np.radians(90)) * radius + node.vertex.y # vertex.y + radius x3 = ( np.cos(np.radians(270)) * radius + node.vertex.x + radius ) # vertex.x + radius z3 = np.sin(np.radians(270)) * radius + node.vertex.y # vertex.y - radius triangle = np.array([[x1, z1], [x2, z2], [x3, z3]]) if node.id in self.system.inclined_roll: angle = self.system.inclined_roll[node.id] triangle = rotate_xy(triangle, angle + np.pi * 0.5) support_patch_poly = mpatches.Polygon( triangle, color=self.plot_colors["support"], zorder=9 ) self.axes[axes_i].add_patch(support_patch_poly) self.axes[axes_i].plot( triangle[1:, 0] - 0.5 * radius * np.sin(angle), triangle[1:, 1] - 0.5 * radius * np.cos(angle), color=self.plot_colors["support"], ) if not rotate: rect_patch_regpoly = mpatches.RegularPolygon( (node.vertex.x, radius - node.vertex.y), numVertices=4, radius=radius, orientation=angle, color=self.plot_colors["support"], zorder=9, fill=False, ) self.axes[axes_i].add_patch(rect_patch_regpoly) elif direction == 2: # horizontal roll support_patch_regpoly = mpatches.RegularPolygon( (node.vertex.x, node.vertex.y - radius), numVertices=3, radius=radius, color=self.plot_colors["support"], zorder=9, ) self.axes[axes_i].add_patch(support_patch_regpoly) y = node.vertex.y - 2 * radius self.axes[axes_i].plot( [node.vertex.x - radius, node.vertex.x + radius], [y, y], color=self.plot_colors["support"], ) if not rotate: rect_patch_rect = mpatches.Rectangle( (node.vertex.x - radius / 2, node.vertex.y - radius / 2), radius, radius, color=self.plot_colors["support"], zorder=9, fill=False, ) self.axes[axes_i].add_patch(rect_patch_rect) elif direction == 1: # vertical roll # translate the support to the node support_patch_poly = mpatches.Polygon( triangle, color=self.plot_colors["support"], zorder=9 ) self.axes[axes_i].add_patch(support_patch_poly) y = node.vertex.y - radius self.axes[axes_i].plot( [node.vertex.x + radius * 1.5, node.vertex.x + radius * 1.5], [y, y + 2 * radius], color=self.plot_colors["support"], ) if not rotate: rect_patch_rect = mpatches.Rectangle( (node.vertex.x - radius / 2, node.vertex.y - radius / 2), radius, radius, color=self.plot_colors["support"], zorder=9, fill=False, ) self.axes[axes_i].add_patch(rect_patch_rect) count += 1
[docs] def __rotating_spring_support_patch(self, max_val: float, axes_i: int = 0) -> None: """Plots the rotational spring supports. Args: max_val (float): Max scale of the plot axes_i (int, optional): Which set of axes to plot on (for multi-plot windows). Defaults to 0. """ radius = PATCH_SIZE * max_val for node, _ in self.system.supports_spring_z: r = np.arange(0, radius, 0.001) theta = 25 * np.pi * r / (0.2 * max_val) x = np.cos(theta) * r + node.vertex.x y = np.sin(theta) * r - radius + node.vertex.y self.axes[axes_i].plot(x, y, color=self.plot_colors["support"], zorder=9) # Triangle support_patch = mpatches.RegularPolygon( (node.vertex.x, node.vertex.y - radius * 3), numVertices=3, radius=radius * 0.9, color=self.plot_colors["support"], zorder=9, ) self.axes[axes_i].add_patch(support_patch)
[docs] def __spring_support_patch(self, max_val: float, axes_i: int = 0) -> None: """Plots the linear spring supports. Args: max_val (float): Max scale of the plot axes_i (int, optional): Which set of axes to plot on (for multi-plot windows). Defaults to 0. """ h = PATCH_SIZE * max_val left = -0.5 * h right = 0.5 * h dh = 0.2 * h for node, _ in self.system.supports_spring_y: yval = np.arange(0, -9, -1) * dh + node.vertex.y xval = ( np.array([0, 0, left, right, left, right, left, 0, 0]) + node.vertex.x ) self.axes[axes_i].plot( xval, yval, color=self.plot_colors["support"], zorder=10 ) # Triangle support_patch = mpatches.RegularPolygon( (node.vertex.x, node.vertex.y - h * 2.6), numVertices=3, radius=h * 0.9, color=self.plot_colors["support"], zorder=10, ) self.axes[axes_i].add_patch(support_patch) for node, _ in self.system.supports_spring_x: xval = np.arange(0, 9, 1, dtype=np.float64) * dh + node.vertex.x yval = ( np.array([0, 0, left, right, left, right, left, 0, 0]) + node.vertex.y ) self.axes[axes_i].plot( xval, yval, color=self.plot_colors["support"], zorder=10 ) # Triangle support_patch = mpatches.RegularPolygon( (node.vertex.x + h * 1.7, node.vertex.y - h), numVertices=3, radius=h * 0.9, color=self.plot_colors["support"], zorder=10, ) self.axes[axes_i].add_patch(support_patch)
[docs] def __internal_hinges_patch(self, max_val: float, axes_i: int = 0) -> None: """Plots the internal hinges. Args: max_val (float): Max scale of the plot axes_i (int, optional): Which set of axes to plot on (for multi-plot windows). Defaults to 0. """ radius = PATCH_SIZE * max_val / 2 for node in self.system.internal_hinges: support_patch = mpatches.Circle( (node.vertex.x, node.vertex.y), radius, edgecolor=self.plot_colors["hinge"], facecolor="w", linewidth=2, zorder=9, ) self.axes[axes_i].add_patch(support_patch)
[docs] def __q_load_patch(self, max_val: float, verbosity: int, axes_i: int = 0) -> None: """Plots the distributed loads. xn1;yn1 q-load xn1;yn1 ------------------- |__________________| x1;y1 element x2;y2 Args: max_val (float): Max scale of the plot verbosity (int): 0: show values and arrows, 1: show load block only axes_i (int, optional): Which set of axes to plot on (for multi-plot windows). Defaults to 0. """ def __plot_patch( h1: float, h2: float, x1: float, y1: float, x2: float, y2: float, ai: float, qi: float, q: float, direction: float, el_angle: float, # pylint: disable=unused-argument ) -> None: """Plots the distributed load patch. Args: h1 (float): start height h2 (float): end height x1 (float): start x coordinate y1 (float): start y coordinate x2 (float): end x coordinate y2 (float): end y coordinate ai (float): angle of the element qi (float): start load magnitude q (float): end load magnitude direction (float): 1 or -1, depending on the direction of the load el_angle (float): angle of the element """ # - value, because the positive y of the system is opposite of positive y of the plotter xn1 = x1 + np.sin(ai) * h1 * direction yn1 = y1 + np.cos(ai) * h1 * direction xn2 = x2 + np.sin(ai) * h2 * direction yn2 = y2 + np.cos(ai) * h2 * direction coordinates = ([x1, xn1, xn2, x2], [y1, yn1, yn2, y2]) self.axes[axes_i].plot(*coordinates, color=self.plot_colors["q_load"]) rec = mpatches.Polygon( np.vstack(coordinates).T, color=self.plot_colors["q_load"], alpha=0.3 ) self.axes[axes_i].add_patch(rec) if verbosity == 0: # arrow # pos = np.sqrt(((y1 - y2) ** 2) + ((x1 - x2) ** 2)) # cg = ((pos / 3) * (qi + 2 * q)) / (qi + q) # height = math.sin(el_angle) * cg # base = math.cos(el_angle) * cg len_x1 = np.sin(ai - np.pi) * 0.6 * h1 * direction len_x2 = np.sin(ai - np.pi) * 0.6 * h2 * direction len_y1 = np.cos(ai - np.pi) * 0.6 * h1 * direction len_y2 = np.cos(ai - np.pi) * 0.6 * h2 * direction step_x = np.linspace(xn1, xn2, 11) step_y = np.linspace(yn1, yn2, 11) step_len_x = np.linspace(len_x1, len_x2, 11) step_len_y = np.linspace(len_y1, len_y2, 11) average_h = (h1 + h2) / 2 # fc = face color, ec = edge color self.axes[axes_i].text( xn1, yn1, f"q={qi}", color=self.plot_colors["q_load_text"], fontsize=9, zorder=10, ) self.axes[axes_i].text( xn2, yn2, f"q={q}", color=self.plot_colors["q_load_text"], fontsize=9, zorder=10, ) # add multiple arrows to fill load for counter, step_xi in enumerate(step_x): if q + qi >= 0: if counter == 0: shape = "right" elif counter == 10: shape = "left" else: shape = "full" else: if counter == 0: shape = "left" elif counter == 10: shape = "right" else: shape = "full" self.axes[axes_i].arrow( step_xi, step_y[counter], step_len_x[counter], step_len_y[counter], head_width=average_h * 0.25, head_length=0.4 * np.sqrt(step_len_y[counter] ** 2 + step_len_x[counter] ** 2), ec=self.plot_colors["q_load_arrow_edge"], fc=self.plot_colors["q_load_arrow_face"], shape=shape, ) for q_id in self.system.loads_q.keys(): el = self.system.element_map[q_id] qi = el.q_load[0] q = el.q_load[1] x1 = el.vertex_1.x y1 = el.vertex_1.y x2 = el.vertex_2.x y2 = el.vertex_2.y if max(qi, q) > 0: direction = 1 else: direction = -1 h1 = 0.05 * max_val * abs(qi) / self.max_q h2 = 0.05 * max_val * abs(q) / self.max_q assert el.q_angle is not None ai = np.pi / 2 - el.q_angle el_angle = el.angle __plot_patch(h1, h2, x1, y1, x2, y2, ai, qi, q, direction, el_angle) if el.q_perp_load[0] != 0 or el.q_perp_load[1] != 0: qi = el.q_perp_load[0] q = el.q_perp_load[1] x1 = el.vertex_1.x + np.sin(ai) * h1 * direction * 2 y1 = el.vertex_1.y + np.cos(ai) * h1 * direction * 2 x2 = el.vertex_2.x + np.sin(ai) * h2 * direction * 2 y2 = el.vertex_2.y + np.cos(ai) * h2 * direction * 2 if max(qi, q) > 0: direction = 1 else: direction = -1 h1 = 0.05 * max_val * abs(qi) / self.max_q h2 = 0.05 * max_val * abs(q) / self.max_q ai = -el.q_angle el_angle = el.angle __plot_patch(h1, h2, x1, y1, x2, y2, ai, qi, q, direction, el_angle)
@staticmethod
[docs] def __arrow_patch_values( Fx: float, Fy: float, node: "Node", h: float ) -> Tuple[float, float, float, float, float]: """Determines the values for the point load arrow patch. Args: Fx (float): Point load magnitude in x direction Fy (float): Point load magnitude in y direction node (Node): Node upon which load is applied h (float): Scale variable Returns: Tuple[float, float, float, float, float]: x, y, len_x, len_y, F (for matplotlib plotter) """ F = (Fx**2 + Fy**2) ** 0.5 len_x = Fx / F * h len_y = -Fy / F * h x = node.vertex.x - len_x * 1.2 y = node.vertex.y - len_y * 1.2 return x, y, len_x, len_y, F
[docs] def __point_load_patch( self, max_plot_range: float, verbosity: int = 0, axes_i: int = 0 ) -> None: """Plots the point loads. Args: max_plot_range (float): Max scale of the plot verbosity (int, optional): 0: show values, 1: show arrow only. Defaults to 0. axes_i (int, optional): Which set of axes to plot on (for multi-plot windows). Defaults to 0. """ for k in self.system.loads_point: Fx, Fy = self.system.loads_point[k] F = (Fx**2 + Fy**2) ** 0.5 node = self.system.node_map[k] h = 0.1 * max_plot_range * F / self.max_system_point_load x, y, len_x, len_y, F = self.__arrow_patch_values(Fx, Fy, node, h) self.axes[axes_i].arrow( x, y, len_x, len_y, head_width=h * 0.15, head_length=0.2 * h, ec=self.plot_colors["point_load_edge"], fc=self.plot_colors["point_load_fill"], zorder=11, ) if verbosity == 0: self.axes[axes_i].text( x, y, f"F={F}", color=self.plot_colors["point_load_text"], fontsize=9, zorder=10, )
[docs] def __moment_load_patch(self, max_val: float, axes_i: int = 0) -> None: """Plots the moment loads. Args: max_val (float): Max scale of the plot axes_i (int, optional): Which set of axes to plot on (for multi-plot windows). Defaults to 0. """ h = 0.2 * max_val for k, v in self.system.loads_moment.items(): node = self.system.node_map[k] if v > 0: self.axes[axes_i].plot( node.vertex.x, node.vertex.y, marker=r"$\circlearrowleft$", ms=25, color=self.plot_colors["moment_load"], ) else: self.axes[axes_i].plot( node.vertex.x, node.vertex.y, marker=r"$\circlearrowright$", ms=25, color=self.plot_colors["moment_load"], ) self.axes[axes_i].text( node.vertex.x + h * 0.2, node.vertex.y + h * 0.2, f"T={v}", color=self.plot_colors["moment_load_text"], fontsize=9, zorder=10, )
[docs] def plot_structure( self, figsize: Optional[Tuple[float, float]], verbosity: int, show: bool = False, supports: bool = True, scale: float = 1, offset: Sequence[float] = (0, 0), gridplot: bool = False, annotations: bool = True, axes_i: int = 0, ) -> Optional["Figure"]: """Plots the structure. Args: figsize (Optional[Tuple[float, float]]): Figure size verbosity (int): 0: show node and element IDs, 1: show structure only show (bool, optional): If True, plt.figure will plot. Defaults to False. supports (bool, optional): If True, supports are plotted. Defaults to True. scale (float, optional): Scale of the plot. Defaults to 1. offset (Sequence[float], optional): Offset of the plot. Defaults to (0, 0). gridplot (bool, optional): If True, the plot will be added to a grid of plots. Defaults to False. annotations (bool, optional): If True, structure annotations are plotted. Defaults to True. axes_i (int, optional): Which set of axes to plot on (for multi-plot windows). Defaults to 0. Returns: Optional[Figure]: Returns figure object if in testing mode, else None """ if not gridplot: figsize = self.__start_plot(figsize) axes_i = 0 else: assert figsize is not None # figsize is always set in gridplots x, y = self.plot_values.structure() max_x: float = np.max(x) min_x: float = np.min(x) max_y: float = np.max(y) min_y: float = np.min(y) center_x = (max_x - min_x) / 2 + min_x + -offset[0] center_y = (max_y - min_y) / 2 + min_y + -offset[1] max_plot_range = max(max_x - min_x, max_y - min_y) ax_range = max_plot_range * scale plusxrange = center_x + ax_range plusyrange = center_y + ax_range * figsize[1] / figsize[0] minxrange = center_x - ax_range minyrange = center_y - ax_range * figsize[1] / figsize[0] self.axes[axes_i].axis((minxrange, plusxrange, minyrange, plusyrange)) for el in self.system.element_map.values(): x_val, y_val = plot_values_element(el) self.axes[axes_i].plot( x_val, y_val, color=self.plot_colors["element"], marker="s" ) if verbosity == 0: # add node ID to plot ax_range = max_plot_range * 0.015 self.axes[axes_i].text( x_val[0] + ax_range, y_val[0] + ax_range, f"{el.node_id1}", color=self.plot_colors["node_number"], fontsize=9, zorder=10, ) self.axes[axes_i].text( x_val[-1] + ax_range, y_val[-1] + ax_range, f"{el.node_id2}", color=self.plot_colors["node_number"], fontsize=9, zorder=10, ) # add element ID to plot factor = 0.02 * self.max_val_structure x_scalar = (x_val[0] + x_val[-1]) / 2 - np.sin(el.angle) * factor y_scalar = (y_val[0] + y_val[-1]) / 2 + np.cos(el.angle) * factor self.axes[axes_i].text( x_scalar, y_scalar, str(el.id), color=self.plot_colors["element_number"], fontsize=9, zorder=10, ) # add element annotation to plot # TO DO: check how this holds with multiple structure scales. if annotations: x_scalar += +np.sin(el.angle) * factor * 2.3 y_scalar += -np.cos(el.angle) * factor * 2.3 self.axes[axes_i].text( x_scalar, y_scalar, el.section_name, color=self.plot_colors["annotation"], fontsize=9, zorder=10, ) # add supports if supports: self.__fixed_support_patch(max_plot_range * scale) self.__hinged_support_patch(max_plot_range * scale) self.__rotational_support_patch(max_plot_range * scale) self.__roll_support_patch(max_plot_range * scale) self.__rotating_spring_support_patch(max_plot_range * scale) self.__spring_support_patch(max_plot_range * scale) self.__internal_hinges_patch(max_plot_range * scale) if verbosity == 0: # add_loads self.__q_load_patch(max_plot_range, verbosity) self.__point_load_patch(max_plot_range, verbosity) self.__moment_load_patch(max_plot_range) if show: self.plot() return None return self.fig
[docs] def _add_node_values( self, x_val: np.ndarray, y_val: np.ndarray, value_1: float, value_2: float, digits: int, axes_i: int = 0, ) -> None: """Adds the node values to the plot. Args: x_val (np.ndarray): X locations y_val (np.ndarray): Y locations value_1 (float): Value of first number value_2 (float): Value of second number digits (int): Number of digits to round to axes_i (int, optional): Which set of axes to plot on (for multi-plot windows). Defaults to 0. """ offset = self.max_val_structure * 0.015 # add value to plot self.axes[axes_i].text( x_val[1] - offset, y_val[1] + offset, f"{round(value_1, digits)}", fontsize=9, ha="center", va="center", ) self.axes[axes_i].text( x_val[-2] - offset, y_val[-2] + offset, f"{round(value_2, digits)}", fontsize=9, ha="center", va="center", )
[docs] def _add_element_values( self, x_val: np.ndarray, y_val: np.ndarray, value: float, index: int, digits: int = 2, axes_i: int = 0, ) -> None: """Adds the element values to the plot. Args: x_val (np.ndarray): X locations y_val (np.ndarray): Y locations value (float): Value of number index (int): Index of value digits (int, optional): Number of digits to round to. Defaults to 2. axes_i (int, optional): Which set of axes to plot on (for multi-plot windows). Defaults to 0. """ self.axes[axes_i].text( x_val[index], y_val[index], f"{round(value, digits)}", fontsize=9, ha="center", va="center", )
[docs] def plot_result( self, axis_values: Sequence, force_1: Optional[float] = None, force_2: Optional[float] = None, digits: int = 2, node_results: bool = True, fill_polygon: bool = True, color: str = "b", axes_i: int = 0, ) -> None: """Plots a single result on the structure. Args: axis_values (Sequence): X and Y values force_1 (Optional[float], optional): First force to plot. Defaults to None. force_2 (Optional[float], optional): Second force to plot. Defaults to None. digits (int, optional): Number of digits to round to. Defaults to 2. node_results (bool, optional): Whether or not to plot nodal results. Defaults to True. fill_polygon (bool, optional): Whether or not to fill a polygon for the result. Defaults to True. color (int, optional): Color index with which to draw. Defaults to 0. axes_i (int, optional): Which set of axes to plot on (for multi-plot windows). Defaults to 0. """ if fill_polygon: rec = mpatches.Polygon(np.vstack(axis_values).T, color=color, alpha=0.3) self.axes[axes_i].add_patch(rec) # plot force x_val = axis_values[0] y_val = axis_values[1] self.axes[axes_i].plot(x_val, y_val, color=color) if node_results and force_1 and force_2: self._add_node_values(x_val, y_val, force_1, force_2, digits, axes_i=axes_i)
[docs] def plot(self) -> None: """Plots the figure.""" plt.show()
[docs] def axial_force( self, factor: Optional[float] = None, figsize: Optional[Tuple[float, float]] = None, verbosity: int = 0, scale: float = 1, offset: Sequence[float] = (0, 0), show: bool = True, gridplot: bool = False, axes_i: int = 0, ) -> Optional["Figure"]: """Plots the axial force. Args: factor (Optional[float], optional): Scaling factor. Defaults to None. figsize (Optional[Tuple[float, float]], optional): Figure size. Defaults to None. verbosity (int, optional): 0: show values, 1: show axial force only. Defaults to 0. scale (float, optional): Scale of the plot. Defaults to 1. offset (Sequence[float], optional): Offset of the plot. Defaults to (0, 0). show (bool, optional): If True, plt.figure will plot. Defaults to False. gridplot (bool, optional): If True, the plot will be added to a grid of plots. Defaults to False. axes_i (int, optional): Which set of axes to plot on (for multi-plot windows). Defaults to 0. Returns: Optional[Figure]: Returns figure object if in testing mode, else None """ self.plot_structure( figsize, 1, scale=scale, offset=offset, gridplot=gridplot, axes_i=axes_i ) assert list(self.system.element_map.values())[0].axial_force is not None con = len(list(self.system.element_map.values())[0].axial_force) if factor is None: max_force = max( map( lambda el: max( abs(el.N_1 or 0.0), abs(el.N_2 or 0.0), abs(((el.all_qn_load[0] + el.all_qn_load[1]) / 16) * el.l**2), ), self.system.element_map.values(), ) ) factor = det_scaling_factor(max_force, self.max_val_structure) for el in self.system.element_map.values(): assert el.N_1 is not None assert el.N_2 is not None if ( math.isclose(el.N_1, 0, rel_tol=1e-5, abs_tol=1e-9) and math.isclose(el.N_2, 0, rel_tol=1e-5, abs_tol=1e-9) and math.isclose(el.all_qn_load[0], 0, rel_tol=1e-5, abs_tol=1e-9) and math.isclose(el.all_qn_load[1], 0, rel_tol=1e-5, abs_tol=1e-9) ): continue axis_values = plot_values_axial_force(el, factor, con) color = ( self.plot_colors["axial_force_neg"] if el.N_1 < 0 else self.plot_colors["axial_force_pos"] ) self.plot_result( axis_values, el.N_1, el.N_2, node_results=not bool(verbosity), color=color, axes_i=axes_i, ) point = (el.vertex_2 - el.vertex_1) / 2 + el.vertex_1 if el.N_1 < 0: point.displace_polar( alpha=el.angle + 0.5 * np.pi, radius=0.5 * el.N_1 * factor, inverse_y_axis=True, ) if verbosity == 0: self.axes[axes_i].text( point.x, point.y, "-", ha="center", va="center", fontsize=20, color=self.plot_colors["axial_force_sign"], ) if el.N_1 > 0: point.displace_polar( alpha=el.angle + 0.5 * np.pi, radius=0.5 * el.N_1 * factor, inverse_y_axis=True, ) if verbosity == 0: self.axes[axes_i].text( point.x, point.y, "+", ha="center", va="center", fontsize=14, color=self.plot_colors["axial_force_sign"], ) if show: self.plot() return None return self.fig
[docs] def bending_moment( self, factor: Optional[float] = None, figsize: Optional[Tuple[float, float]] = None, verbosity: int = 0, scale: float = 1, offset: Sequence[float] = (0, 0), show: bool = True, gridplot: bool = False, axes_i: int = 0, ) -> Optional["Figure"]: """Plots the bending moment. Args: factor (Optional[float], optional): Scaling factor. Defaults to None. figsize (Optional[Tuple[float, float]], optional): Figure size. Defaults to None. verbosity (int, optional): 0: show values, 1: show bending moment only. Defaults to 0. scale (float, optional): Scale of the plot. Defaults to 1. offset (Sequence[float], optional): Offset of the plot. Defaults to (0, 0). show (bool, optional): If True, plt.figure will plot. Defaults to False. gridplot (bool, optional): If True, the plot will be added to a grid of plots. Defaults to False. axes_i (int, optional): Which set of axes to plot on (for multi-plot windows). Defaults to 0. Returns: Optional[Figure]: Returns figure object if in testing mode, else None """ self.plot_structure( figsize, 1, scale=scale, offset=offset, gridplot=gridplot, axes_i=axes_i ) assert list(self.system.element_map.values())[0].bending_moment is not None con = len(list(self.system.element_map.values())[0].bending_moment) if factor is None: # maximum moment determined by comparing the node's moments and the sagging moments. max_moment = max( map( lambda el: ( max( abs(el.node_1.Tz), abs(el.node_2.Tz), abs( ((el.all_qp_load[0] + el.all_qp_load[1]) / 16) * el.l**2 ), ) if el.type == "general" else 0 ), self.system.element_map.values(), ) ) factor = det_scaling_factor(max_moment, self.max_val_structure) # determine the axis values for el in self.system.element_map.values(): if ( math.isclose(el.node_1.Tz, 0, rel_tol=1e-5, abs_tol=1e-9) and math.isclose(el.node_2.Tz, 0, rel_tol=1e-5, abs_tol=1e-9) and math.isclose(el.all_qp_load[0], 0, rel_tol=1e-5, abs_tol=1e-9) and math.isclose(el.all_qp_load[1], 0, rel_tol=1e-5, abs_tol=1e-9) ): # If True there is no bending moment, so no need for plotting. continue axis_values = plot_values_bending_moment(el, factor, con) node_results = verbosity == 0 self.plot_result( axis_values, abs(el.node_1.Tz), abs(el.node_2.Tz), node_results=node_results, color=self.plot_colors["bending_moment"], axes_i=axes_i, ) if el.all_qp_load: assert el.bending_moment is not None m_sag = min(el.bending_moment) index = find_nearest(el.bending_moment, m_sag)[1] offset1 = self.max_val_structure * -0.05 if verbosity == 0: x = axis_values[0][index] + np.sin(-el.angle) * offset1 y = axis_values[1][index] + np.cos(-el.angle) * offset1 self.axes[axes_i].text(x, y, f"{round(m_sag, 1)}", fontsize=9) if show: self.plot() return None return self.fig
[docs] def shear_force( self, factor: Optional[float] = None, figsize: Optional[Tuple[float, float]] = None, verbosity: int = 0, scale: float = 1, offset: Sequence[float] = (0, 0), show: bool = True, gridplot: bool = False, include_structure: bool = True, axes_i: int = 0, ) -> Optional["Figure"]: """Plots the shear force. Args: factor (Optional[float], optional): Scaling factor. Defaults to None. figsize (Optional[Tuple[float, float]], optional): Figure size. Defaults to None. verbosity (int, optional): 0: show values, 1: show shear force only. Defaults to 0. scale (float, optional): Scale of the plot. Defaults to 1. offset (Sequence[float], optional): Offset of the plot. Defaults to (0, 0). show (bool, optional): If True, plt.figure will plot. Defaults to False. gridplot (bool, optional): If True, the plot will be added to a grid of plots. Defaults to False. include_structure (bool, optional): If True, the structure will be plotted. Defaults to True. axes_i (int, optional): Which set of axes to plot on (for multi-plot windows). Defaults to 0. Returns: Optional[Figure]: Returns figure object if in testing mode, else None """ if include_structure: self.plot_structure( figsize, 1, scale=scale, offset=offset, gridplot=gridplot, axes_i=axes_i ) if factor is None: max_force = max( map( lambda el: ( np.max(np.abs(el.shear_force)) if el.shear_force is not None else 0.0 ), self.system.element_map.values(), ) ) factor = det_scaling_factor(max_force, self.max_val_structure) for el in self.system.element_map.values(): if ( math.isclose(el.node_1.Tz, 0, rel_tol=1e-5, abs_tol=1e-9) and math.isclose(el.node_2.Tz, 0, rel_tol=1e-5, abs_tol=1e-9) and math.isclose(el.all_qp_load[0], 0, rel_tol=1e-5, abs_tol=1e-9) and math.isclose(el.all_qp_load[1], 0, rel_tol=1e-5, abs_tol=1e-9) ): # If True there is no bending moment and no shear, thus no shear force, # so no need for plotting. continue axis_values = plot_values_shear_force(el, factor) assert el.shear_force is not None shear_1 = el.shear_force[0] shear_2 = el.shear_force[-1] self.plot_result( axis_values, shear_1, shear_2, node_results=not bool(verbosity), color=self.plot_colors["shear_force"], axes_i=axes_i, ) if show: self.plot() return None return self.fig
[docs] def reaction_force( self, figsize: Optional[Tuple[float, float]], verbosity: int, scale: float, offset: Sequence[float], show: bool, gridplot: bool = False, axes_i: int = 0, ) -> Optional["Figure"]: """Plots the reaction forces. Args: figsize (Optional[Tuple[float, float]]): Figure size verbosity (int): 0: show node and element IDs, 1: show structure only scale (float): Scale of the plot offset (Sequence[float]): Offset of the plot show (bool): If True, plt.figure will plot gridplot (bool, optional): If True, the plot will be added to a grid of plots. Defaults to False. axes_i (int, optional): Which set of axes to plot on (for multi-plot windows). Defaults to 0. Returns: Optional[Figure]: Returns figure object if in testing mode, else None """ self.plot_structure( figsize, 1, supports=False, scale=scale, offset=offset, gridplot=gridplot, axes_i=axes_i, ) h = 0.2 * self.max_val_structure max_force = max( map( lambda node: max(abs(node.Fx), abs(node.Fy)), self.system.reaction_forces.values(), ) ) for node in self.system.reaction_forces.values(): if not math.isclose(node.Fx, 0, rel_tol=1e-5, abs_tol=1e-9): # x direction scale = abs(node.Fx) / max_force * h sol = self.__arrow_patch_values(node.Fx, 0, node, scale) x = sol[0] y = sol[1] len_x = sol[2] len_y = sol[3] self.axes[axes_i].arrow( x, y, len_x, len_y, head_width=h * 0.15, head_length=0.2 * scale, ec=self.plot_colors["reaction_force_arrow_edge"], fc=self.plot_colors["reaction_force_arrow_fill"], zorder=11, ) if verbosity == 0: self.axes[axes_i].text( x, y, f"R={round(node.Fx, 2)}", color=self.plot_colors["reaction_force_text"], fontsize=9, zorder=10, ) if not math.isclose(node.Fy, 0, rel_tol=1e-5, abs_tol=1e-9): # y direction scale = abs(node.Fy) / max_force * h sol = self.__arrow_patch_values(0, node.Fy, node, scale) x = sol[0] y = sol[1] len_x = sol[2] len_y = sol[3] self.axes[axes_i].arrow( x, y, len_x, len_y, head_width=h * 0.15, head_length=0.2 * scale, ec=self.plot_colors["reaction_force_arrow_edge"], fc=self.plot_colors["reaction_force_arrow_fill"], zorder=11, ) if verbosity == 0: self.axes[axes_i].text( x, y, f"R={round(node.Fy, 2)}", color=self.plot_colors["reaction_force_text"], fontsize=9, zorder=10, ) if not math.isclose(node.Tz, 0, rel_tol=1e-5, abs_tol=1e-9): # '$...$': render the strings using mathtext if node.Tz > 0: self.axes[axes_i].plot( node.vertex.x, node.vertex.y, marker=r"$\circlearrowleft$", ms=25, color=self.plot_colors["reaction_force_arrow_fill"], ) if node.Tz < 0: self.axes[axes_i].plot( node.vertex.x, node.vertex.y, marker=r"$\circlearrowright$", ms=25, color=self.plot_colors["reaction_force_arrow_fill"], ) if verbosity == 0: self.axes[axes_i].text( node.vertex.x + h * 0.2, node.vertex.y + h * 0.2, f"T={round(node.Tz, 2)}", color=self.plot_colors["reaction_force_text"], fontsize=9, zorder=10, ) if show: self.plot() return None return self.fig
[docs] def displacements( # pylint: disable=arguments-renamed self, factor: Optional[float] = None, figsize: Optional[Tuple[float, float]] = None, verbosity: int = 0, scale: float = 1, offset: Sequence[float] = (0, 0), show: bool = True, linear: bool = False, gridplot: bool = False, axes_i: int = 0, ) -> Optional["Figure"]: """Plots the displacements. Args: factor (Optional[float], optional): Scaling factor. Defaults to None. figsize (Optional[Tuple[float, float]], optional): Figure size. Defaults to None. verbosity (int, optional): 0: show values, 1: show displacements only. Defaults to 0. scale (float, optional): Scale of the plot. Defaults to 1. offset (Sequence[float], optional): Offset of the plot. Defaults to (0, 0). show (bool, optional): If True, plt.figure will plot. Defaults to False. linear (bool, optional): If True, the bending in between the elements is determined. Defaults to False. gridplot (bool, optional): If True, the plot will be added to a grid of plots. Defaults to False. axes_i (int, optional): Which set of axes to plot on (for multi-plot windows). Defaults to 0. Returns: Optional[Figure]: Returns figure object if in testing mode, else None """ self.plot_structure( figsize, 1, scale=scale, offset=offset, gridplot=gridplot, axes_i=axes_i ) if factor is None: # needed to determine the scaling factor max_displacement = max( map( lambda el: ( np.sqrt( (el.max_extension or 0) ** 2 + (el.max_total_deflection or 0) ** 2 ) ), self.system.element_map.values(), ) ) factor = det_scaling_factor(max_displacement, self.max_val_structure) for el in self.system.element_map.values(): axis_values = plot_values_deflection(el, factor, linear) self.plot_result( axis_values, node_results=False, fill_polygon=False, color=self.plot_colors["displaced_elem"], axes_i=axes_i, ) if el.type == "general": assert el.deflection is not None # index of the max deflection x = np.linspace(el.vertex_1.x, el.vertex_2.x, el.deflection.size) y = np.linspace(el.vertex_1.y, el.vertex_2.y, el.deflection.size) xd, yd = plot_values_deflection(el, 1.0, linear) deflection = ((xd - x) ** 2 + (yd - y) ** 2) ** 0.5 index = int(np.argmax(np.abs(deflection))) if verbosity == 0: if index != 0 or index != el.deflection.size: self._add_element_values( axis_values[0], axis_values[1], deflection[index], index, 3, axes_i=axes_i, ) if show: self.plot() return None return self.fig
[docs] def results_plot( self, figsize: Optional[Tuple[float, float]], verbosity: int, scale: float, offset: Sequence[float], show: bool, ) -> Optional["Figure"]: """Plots all the results in one gridded figure. Args: figsize (Optional[Tuple[float, float]]): Figure size verbosity (int): 0: show values, 1: show arrows and polygons only scale (float): Scale of the plot offset (Sequence[float]): Offset of the plot show (bool): If True, plt.figure will plot Returns: Optional[Figure]: Returns figure object if in testing mode, else None """ plt.close("all") self.axes = [] self.fig = plt.figure(figsize=figsize) a = 320 self.axes.append(self.fig.add_subplot(a + 1)) plt.title("structure") self.plot_structure( figsize, verbosity, show=False, scale=scale, offset=offset, gridplot=True, axes_i=0, ) self.axes.append(self.fig.add_subplot(a + 2)) print(self.axes) plt.title("bending moment") self.bending_moment( factor=None, figsize=figsize, verbosity=verbosity, scale=scale, offset=offset, show=False, gridplot=True, axes_i=1, ) self.axes.append(self.fig.add_subplot(a + 3)) plt.title("shear force") self.shear_force( factor=None, figsize=figsize, verbosity=verbosity, scale=scale, offset=offset, show=False, gridplot=True, axes_i=2, ) self.axes.append(self.fig.add_subplot(a + 4)) plt.title("axial force") self.axial_force( factor=None, figsize=figsize, verbosity=verbosity, scale=scale, offset=offset, show=False, gridplot=True, axes_i=3, ) self.axes.append(self.fig.add_subplot(a + 5)) plt.title("displacements") self.displacements( factor=None, figsize=figsize, verbosity=verbosity, scale=scale, offset=offset, show=False, linear=False, gridplot=True, axes_i=4, ) self.axes.append(self.fig.add_subplot(a + 6)) plt.title("reaction force") self.reaction_force( figsize=figsize, verbosity=verbosity, scale=scale, offset=offset, show=False, gridplot=True, axes_i=5, ) if show: self.plot() return None return self.fig