From da899c827607fe579515821e26e69f59ae532407 Mon Sep 17 00:00:00 2001 From: Brooks Smith Date: Tue, 10 Oct 2023 12:47:22 +1100 Subject: [PATCH 01/20] Round 1 of adding docstrings to all functions --- anastruct/basic.py | 82 +- anastruct/cython/basic.py | 22 +- anastruct/fem/elements.py | 158 +++- anastruct/fem/node.py | 8 +- anastruct/fem/postprocess.py | 9 +- anastruct/fem/system.py | 955 +++++++++++--------- anastruct/fem/system_components/assembly.py | 120 ++- anastruct/fem/system_components/solver.py | 8 +- anastruct/fem/system_components/util.py | 94 +- anastruct/fem/util/load.py | 6 +- anastruct/vertex.py | 126 ++- 11 files changed, 1008 insertions(+), 580 deletions(-) diff --git a/anastruct/basic.py b/anastruct/basic.py index 4a5865b7..4d7dfba3 100644 --- a/anastruct/basic.py +++ b/anastruct/basic.py @@ -1,4 +1,3 @@ -import collections.abc from typing import Any, Sequence, Tuple import numpy as np @@ -12,32 +11,58 @@ from anastruct.cython.basic import angle_x_axis, converge -def find_nearest(array: np.ndarray, value: float) -> Tuple[float, int]: - """ - :param array: (numpy array object) - :param value: (float) value searched for - :return: (tuple) nearest value, index +def find_nearest(array: np.ndarray[float, Any], value: float) -> Tuple[float, int]: + """Find the nearest value in an array + + Args: + array (np.ndarray[float, Any]): array to search within + value (float): value to search for + + Returns: + Tuple[float, int]: Nearest value, index """ + # Subtract the value of the value's in the array. Make the values absolute. # The lowest value is the nearest. - index = (np.abs(array - value)).argmin() + index: int = (np.abs(array - value)).argmin() return array[index], index def integrate_array(y: np.ndarray, dx: float) -> np.ndarray: - """ - integrate array y * dx + """Integrate an array y*dx using the trapezoidal rule + + Args: + y (np.ndarray): Array to integrate + dx (float): Step size + + Returns: + np.ndarray: Integrated array """ return np.cumsum(y) * dx # type: ignore class FEMException(Exception): def __init__(self, type_: str, message: str): + """Exception for FEM + + Args: + type_ (str): Type of exception + message (str): Message for exception + """ self.type = type_ self.message = message def arg_to_list(arg: Any, n: int) -> list: + """Convert an argument to a list of length n + + Args: + arg (Any): Argument to convert + n (int): Length of list to create + + Returns: + list: List of length n + """ if isinstance(arg, Sequence) and not isinstance(arg, str) and len(arg) == n: return list(arg) if isinstance(arg, Sequence) and not isinstance(arg, str) and len(arg) == 1: @@ -45,32 +70,14 @@ def arg_to_list(arg: Any, n: int) -> list: return [arg for _ in range(n)] -def args_to_lists(*args: list) -> list: - arg_lists = [] - for arg in args: - if isinstance(arg, collections.abc.Iterable) and not isinstance(arg, str): - arg_lists.append(arg) - else: - arg_lists.append([arg]) - lengths = list(map(len, arg_lists)) - n = max(lengths) - if n == 1: - return arg_lists - - args_return = [] - for arg, l in zip(arg_lists, lengths): - if l == n: - args_return.append(arg) - else: - args_return.append([arg[0] for _ in range(n)]) - return args_return - - def rotation_matrix(angle: float) -> np.ndarray: - """ + """Create a 2x2 rotation matrix - :param angle: (flt) angle in radians - :return: rotated euclidean xy matrix + Args: + angle (float): Angle in radians + + Returns: + np.ndarray: 2x2 Euclidean rotation matrix """ s = np.sin(angle) c = np.cos(angle) @@ -78,6 +85,15 @@ def rotation_matrix(angle: float) -> np.ndarray: def rotate_xy(a: np.ndarray, angle: float) -> np.ndarray: + """Rotate a 2D matrix around the origin + + Args: + a (np.ndarray): Matrix to rotate + angle (float): Angle in radians + + Returns: + np.ndarray: Rotated matrix + """ b = np.array(a) b[:, 0] -= a[0, 0] b[:, 1] -= a[0, 1] diff --git a/anastruct/cython/basic.py b/anastruct/cython/basic.py index fcb184cd..f9c639e4 100644 --- a/anastruct/cython/basic.py +++ b/anastruct/cython/basic.py @@ -2,13 +2,14 @@ def converge(lhs: float, rhs: float) -> float: - """ - Determine convergence factor. + """Determine convergence factor + + Args: + lhs (float): The left-hand side of the equation + rhs (float): The right-hand side of the equation - :param lhs: (flt) - :param rhs: (flt) - :param div: (flt) - :return: multiplication factor (flt) ((lhs / rhs) - 1) / div + 1 + Returns: + float: Convergence factor ((lhs / rhs) - 1) / div + 1 """ lhs = abs(lhs) rhs = abs(rhs) @@ -18,6 +19,15 @@ def converge(lhs: float, rhs: float) -> float: def angle_x_axis(delta_x: float, delta_z: float) -> float: + """Determine the angle of the element with the global x-axis + + Args: + delta_x (float): Element length in the x-direction + delta_z (float): Element length in the z-direction + + Returns: + float: Angle of the element with the global x-axis + """ # dot product v_x = [1, 0] ; v = [delta_x, delta_z] # dot product = 1 * delta_x + 0 * delta_z -> delta_x ai = math.acos(delta_x / math.sqrt(delta_x**2 + delta_z**2)) diff --git a/anastruct/fem/elements.py b/anastruct/fem/elements.py index aa62d6c0..20accf23 100644 --- a/anastruct/fem/elements.py +++ b/anastruct/fem/elements.py @@ -3,7 +3,7 @@ import copy from functools import lru_cache from math import cos, sin -from typing import TYPE_CHECKING, Dict, List, Optional +from typing import TYPE_CHECKING, Dict, List, Literal, Optional import numpy as np @@ -12,6 +12,7 @@ if TYPE_CHECKING: from anastruct.fem.node import Node from anastruct.fem.system import Spring + from anastruct.types import ElementType from anastruct.vertex import Vertex try: @@ -40,21 +41,24 @@ def __init__( angle: float, vertex_1: Vertex, vertex_2: Vertex, - type_: str, + type_: ElementType, section_name: str, spring: Optional[Spring] = None, ): - """ - :param id_: integer representing the elements ID - :param EA: Young's modulus * Area - :param EI: Young's modulus * Moment of Inertia - :param l: length - :param angle: angle between element and x-axis - :param vertex_1: point object - :param vertex_2: point object - :param spring: (dict) Set a spring at node 1 or node 2. - spring={1: k - 2: k} + """Create an element object + + Args: + id_ (int): Integer representing the elements ID + EA (float): Axial stiffness + EI (float): Bending stiffness + l (float): Length + angle (float): Angle between element and x-axis + vertex_1 (Vertex): Starting Vertex + vertex_2 (Vertex): Ending Vertex + type_ (str): + section_name (str): Section name (for element annotation) + spring (Optional[Spring], optional): Set a spring at node 1 or node 2. + spring={1: k, 2: k}. Defaults to None. """ self.id = id_ self.type = type_ @@ -91,12 +95,17 @@ def __init__( self.max_deflection = None self.max_extension = None self.nodes_plastic: List[bool] = [False, False] - self.compile_constitutive_matrix(EA, EI, l, spring) + self.compile_constitutive_matrix() self.compile_stiffness_matrix() self.section_name = section_name # needed for element annotation @property def all_qp_load(self) -> List[float]: + """All parallel q (distributed) loads + + Returns: + List[float]: All parallel q (distributed) loads + """ if self.q_angle is not None: q_factor = sin(self.q_angle - self.angle) q_perp_factor = cos(self.q_angle - self.angle) @@ -112,6 +121,11 @@ def all_qp_load(self) -> List[float]: @property def all_qn_load(self) -> List[float]: + """All normal q (distributed) loads + + Returns: + List[float]: All normal q (distributed) loads + """ if self.q_angle is not None: q_factor = -cos(self.q_angle - self.angle) q_perp_factor = sin(self.q_angle - self.angle) @@ -127,16 +141,28 @@ def all_qn_load(self) -> List[float]: @property def node_1(self) -> Node: + """Starting node + + Returns: + Node: Starting node + """ return self.node_map[self.node_id1] @property def node_2(self) -> Node: + """Ending node + + Returns: + Node: Ending node + """ return self.node_map[self.node_id2] @property def hinges(self) -> List[int]: - """ - Node ids of hinges on element + """Node IDs of hinges + + Returns: + List[int]: Node IDs of hinges """ out = [] @@ -150,33 +176,39 @@ def hinges(self) -> List[int]: return out def determine_force_vector(self) -> Optional[np.ndarray]: + """Determine the force vector of the element + + Returns: + Optional[np.ndarray]: Force vector of the element + """ self.element_force_vector = np.dot( self.stiffness_matrix, self.element_displacement_vector ) return self.element_force_vector def compile_stiffness_matrix(self) -> None: + """Compile the stiffness matrix of the element""" self.stiffness_matrix = stiffness_matrix( self.constitutive_matrix, self.kinematic_matrix ) - def compile_kinematic_matrix(self, a1: float, a2: float, l: float) -> None: - self.kinematic_matrix = kinematic_matrix(a1, a2, l) + def compile_kinematic_matrix(self) -> None: + """Compile the kinematic matrix of the element""" + self.kinematic_matrix = kinematic_matrix(self.a1, self.a2, self.l) - def compile_constitutive_matrix( - self, - EA: float, - EI: float, - l: float, - spring: Optional[Dict[int, float]] = None, - node_1_hinge: Optional[bool] = False, - node_2_hinge: Optional[bool] = False, - ) -> None: + def compile_constitutive_matrix(self) -> None: + """Compile the constitutive matrix of the element""" self.constitutive_matrix = constitutive_matrix( - EA, EI, l, spring, node_1_hinge, node_2_hinge + self.EA, self.EI, self.l, self.springs, self.node_1.hinge, self.node_2.hinge ) - def update_stiffness(self, factor: float, node: int) -> None: + def update_stiffness(self, factor: float, node: Literal[1, 2]) -> None: + """Update the stiffness matrix of the element + + Args: + factor (float): Factor to multiply the stiffness matrix with + node (Literal[1, 2]): Node ID of the node to update (1 or 2) + """ if node == 1: self.constitutive_matrix[1][1] *= factor self.constitutive_matrix[1][2] *= factor @@ -188,6 +220,7 @@ def update_stiffness(self, factor: float, node: int) -> None: self.compile_stiffness_matrix() def compile_geometric_non_linear_stiffness_matrix(self) -> None: + """Compile the geometric non-linear stiffness matrix of the element""" self.compile_stiffness_matrix() assert self.N_1 is not None self.stiffness_matrix += geometric_stiffness_matrix( @@ -195,10 +228,22 @@ def compile_geometric_non_linear_stiffness_matrix(self) -> None: ) def reset(self) -> None: + """Reset the element's solve state""" self.element_displacement_vector = np.zeros(6) self.element_primary_force_vector = np.zeros(6) def __add__(self, other: Element) -> Element: + """Add two elements + + Args: + other (Element): Element to add + + Raises: + FEMException: If the elements have different IDs + + Returns: + Element: Sum of the two elements + """ if self.id != other.id: raise FEMException( "Wrong element:", "only elements with the same id can be added." @@ -239,11 +284,15 @@ def __add__(self, other: Element) -> Element: @lru_cache(CACHE_BOUND) def kinematic_matrix(a1: float, a2: float, l: float) -> np.ndarray: - """ - Kinematic matrix of an element dependent of the angle ai and the length of the element. + """Generate the kinematic matrix of an element - :param a1: (float) angle with respect to the x axis. - :param l: (float) Length + Args: + a1 (float): Angle of the element at node 1 + a2 (float): Angle of the element at node 2 + l (float): Length of the element + + Returns: + np.ndarray: Kinematic matrix of the element """ c1 = cos(a1) s1 = sin(a1) @@ -262,16 +311,22 @@ def constitutive_matrix( EA: float, EI: float, l: float, - spring: Optional[Dict[int, float]], + spring: Optional["Spring"], node_1_hinge: Optional[bool], node_2_hinge: Optional[bool], ) -> np.ndarray: - """ - :param EA: (float) Young's modules * Area - :param EI: (float) Young's modules * Moment of Inertia - :param l: (float) Length - :param spring: (int) 1 or 2. Apply a hinge on the first of the second node. - :return: (array) + """Generate the constitutive matrix of an element + + Args: + EA (float): Axial stiffness + EI (float): Bending stiffness + l (float): Length + spring (Optional[Spring]): Spring stiffnesses at node 1 and node 2 + node_1_hinge (Optional[bool]): Whether node 1 is a hinge + node_2_hinge (Optional[bool]): Whether node 2 is a hinge + + Returns: + np.ndarray: Constitutive matrix of the element """ matrix = np.array( [[EA / l, 0, 0], [0, 4 * EI / l, -2 * EI / l], [0, -2 * EI / l, 4 * EI / l]] @@ -308,6 +363,15 @@ def constitutive_matrix( def stiffness_matrix( var_constitutive_matrix: np.ndarray, var_kinematic_matrix: np.ndarray ) -> np.ndarray: + """Generate the stiffness matrix of an element + + Args: + var_constitutive_matrix (np.ndarray): Constitutive matrix of the element + var_kinematic_matrix (np.ndarray): Kinematic matrix of the element + + Returns: + np.ndarray: Stiffness matrix of the element + """ kinematic_transposed_times_constitutive = ( var_kinematic_matrix.transpose() @ var_constitutive_matrix ) @@ -315,12 +379,16 @@ def stiffness_matrix( def geometric_stiffness_matrix(l: float, N: float, a1: float, a2: float) -> np.ndarray: - """ + """Generate the geometric stiffness matrix of an element + + Args: + l (float): Length + N (float): Axial force + a1 (float): Angle of the element at node 1 + a2 (float): Angle of the element at node 2 - :param l: (float) Length. - :param N: (float) Axial force. - :param a1: (float) angle. (First try 1st order) - :return: (array) + Returns: + np.ndarray: Geometric stiffness matrix of the element """ c1 = cos(a1) s1 = sin(a1) diff --git a/anastruct/fem/node.py b/anastruct/fem/node.py index 7e9d2b1a..7c7b92d6 100644 --- a/anastruct/fem/node.py +++ b/anastruct/fem/node.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Dict, Optional +from typing import TYPE_CHECKING, Dict from anastruct.vertex import Vertex @@ -15,9 +15,9 @@ def __init__( Fx: float = 0.0, Fz: float = 0.0, Ty: float = 0.0, - ux: Optional[float] = 0.0, - uz: Optional[float] = 0.0, - phi_y: Optional[float] = 0, + ux: float = 0.0, + uz: float = 0.0, + phi_y: float = 0.0, vertex: Vertex = Vertex(0, 0), hinge: bool = False, ): diff --git a/anastruct/fem/postprocess.py b/anastruct/fem/postprocess.py index 2fba63d0..aa3ebdeb 100644 --- a/anastruct/fem/postprocess.py +++ b/anastruct/fem/postprocess.py @@ -78,9 +78,9 @@ def reaction_forces(self) -> None: node.Fx *= -1 node.Fz *= -1 node.Ty *= -1 - node.ux = None - node.uz = None - node.phi_y = None + node.ux = 0.0 + node.uz = 0.0 + node.phi_y = 0.0 def element_results(self) -> None: """ @@ -210,6 +210,7 @@ def determine_shear_force(element: "Element", con: int) -> None: """ iteration_factor = np.linspace(0, 1, con) x = iteration_factor * element.l + assert element.bending_moment is not None eq = np.polyfit(x, element.bending_moment, 3) shear_force = eq[0] * 3 * x**2 + eq[1] * 2 * x + eq[2] element.shear_force = shear_force @@ -274,7 +275,7 @@ def determine_displacements(element: "Element", con: int) -> None: phi_neg2 = -integrate_array(element.axial_force[::-1], dx) / element.EA u2 = integrate_array(phi_neg2, dx) - element.extension = -(u1 + u2) / 2.0 + element.extension = -1 * (u1 + u2) / 2.0 element.max_extension = np.max(np.abs(element.extension)) # assert element.N_1 is not None diff --git a/anastruct/fem/system.py b/anastruct/fem/system.py index 1e6eee2a..20339ae1 100644 --- a/anastruct/fem/system.py +++ b/anastruct/fem/system.py @@ -2,18 +2,8 @@ import copy import math import re -from typing import ( - TYPE_CHECKING, - Any, - Collection, - Dict, - List, - Optional, - Sequence, - Set, - Tuple, - Union, -) +from typing import (TYPE_CHECKING, Any, Collection, Dict, List, Literal, + Optional, Sequence, Set, Tuple, Union) import numpy as np @@ -31,26 +21,39 @@ from matplotlib.figure import Figure from anastruct.fem.node import Node - - -Spring = Dict[int, float] -MpType = Dict[int, float] + from anastruct.types import (AxisNumber, Dimension, LoadDirection, MpType, + Spring, SupportDirection) class SystemElements: """ Modelling any structure starts with an object of this class. - :ivar EA: Standard axial stiffness of elements, default=15,000 - :ivar EI: Standard bending stiffness of elements, default=5,000 - :ivar figsize: (tpl) Matplotlibs standard figure size - :ivar element_map: (dict) Keys are the element ids, values are the element objects - :ivar node_map: (dict) Keys are the node ids, values are the node objects. - :ivar node_element_map: (dict) maps node ids to element objects. - :ivar loads_point: (dict) Maps node ids to point loads. - :ivar loads_q: (dict) Maps element ids to q-loads. - :ivar loads_moment: (dict) Maps node ids to moment loads. - :ivar loads_dead_load: (set) Element ids that have a dead load applied. + Attributes: + EA: Standard axial stiffness of elements, default=15,000 + EI: Standard bending stiffness of elements, default=5,000 + figsize: (tpl) Matplotlibs standard figure size + element_map: (dict) Keys are the element ids, values are the element objects + node_map: (dict) Keys are the node ids, values are the node objects. + node_element_map: (dict) maps node ids to element objects. + loads_point: (dict) Maps node ids to point loads. + loads_q: (dict) Maps element ids to q-loads. + loads_moment: (dict) Maps node ids to moment loads. + loads_dead_load: (set) Element ids that have a dead load applied. + + Methods: + add_element: Add a new element to the structure. + add_element_grid: Add multiple elements in a grid shape. + add_multiple_elements: Add multiple elements defined by the first and the last point. + add_truss_element: Add a new truss element (an element that only has axial force) to the structure. + add_support_hinged: Add a hinged support to a node. + add_support_fixed: Add a fixed support to a node. + add_support_roll: Add a roll support to a node. + add_support_rotational: Add a rotational support to a node. + add_support_spring: Add a spring support to a node. + insert_node: Insert a node into an existing structure. + solve: Compute the results of current model. + validate: Validate the current model. """ def __init__( @@ -61,18 +64,14 @@ def __init__( load_factor: float = 1.0, mesh: int = 50, ): - """ - * E = Young's modulus - * A = Area - * I = Moment of Inertia - - :param figsize: Set the standard plotting size. - :param EA: Standard E * A. Set the standard values of EA if none provided when - generating an element. - :param EI: Standard E * I. Set the standard values of EA if none provided when - generating an element. - :param load_factor: Multiply all loads with this factor. - :param mesh: Plotting mesh. Has no influence on the calculation. + """Create a new structure + + Args: + figsize (Tuple[float, float], optional): Figure size to generate. Defaults to (12, 8). + EA (float, optional): Axial stiffness. Defaults to 15e3. + EI (float, optional): Bending stiffness. Defaults to 5e3. + load_factor (float, optional): Load factor by which to multiply all loads. Defaults to 1.0. + mesh (int, optional): Number of mesh elements, only used for plotting. Defaults to 50. """ # init object self.post_processor = post_sl(self) @@ -111,7 +110,7 @@ def __init__( self.supports_spring_x: List[Tuple[Node, bool]] = [] self.supports_spring_z: List[Tuple[Node, bool]] = [] self.supports_spring_y: List[Tuple[Node, bool]] = [] - self.supports_roll_direction: List[int] = [] + self.supports_roll_direction: List[Literal[1, 2, 3]] = [] self.inclined_roll: Dict[ int, float ] = {} # map node ids to inclination angle relative to global x-axis. @@ -136,7 +135,7 @@ def __init__( self.reaction_forces: Dict[int, Node] = {} # node objects self.non_linear = False self.non_linear_elements: Dict[ - int, Dict[int, float] + int, Dict[Literal[1, 2], float] ] = ( {} ) # keys are element ids, values are dicts: {node_index: max moment capacity} @@ -160,47 +159,84 @@ def __init__( @property def id_last_element(self) -> int: + """ID of the last element added to the structure + + Returns: + int: ID of the last element added to the structure + """ return max(self.element_map.keys()) @property def id_last_node(self) -> int: + """ID of the last node added to the structure + + Returns: + int: ID of the last node added to the structure + """ return max(self.node_map.keys()) def add_element_grid( self, x: Union[List[float], np.ndarray], y: Union[List[float], np.ndarray], - EA: Optional[Union[List[float], np.ndarray]] = None, - EI: Optional[Union[List[float], np.ndarray]] = None, - g: Optional[Union[List[float], np.ndarray]] = None, - mp: Optional[MpType] = None, - spring: Optional[Spring] = None, + EA: Optional[Union[List[float], np.ndarray, float]] = None, + EI: Optional[Union[List[float], np.ndarray, float]] = None, + g: Optional[Union[List[float], np.ndarray, float]] = None, + mp: Optional["MpType"] = None, + spring: Optional["Spring"] = None, **kwargs: dict, ) -> None: - """ - Add multiple elements defined by two containers with coordinates. - - :param x: x coordinates. - :param y: y coordinates. - :param EA: See 'add_element' method - :param EI: See 'add_element' method - :param g: See 'add_element' method - :param mp: See 'add_element' method - :param spring: See 'add_element' method - :paramg **kwargs: See 'add_element' method - :return: None - """ - a = np.ones(len(x)) + """Add multiple elements in a grid shape + + Args: + x (Union[List[float], np.ndarray]): X coordinates of the grid + y (Union[List[float], np.ndarray]): Y coordinates of the grid + EA (Optional[Union[List[float], np.ndarray, float]], optional): Axial stiffnesses. Defaults to None. + EI (Optional[Union[List[float], np.ndarray, float]], optional): Bending stiffnesses. Defaults to None. + g (Optional[Union[List[float], np.ndarray, float]], optional): Self-weights. Defaults to None. + mp (Optional[MpType], optional): Maximum plastic moment capacities for all elements. Defaults to None. + spring (Optional[Spring], optional): Springs for all elements. Defaults to None. + + Raises: + FEMException: x and y should have the same length. + FEMException: The mp parameter should be a dictionary. + """ + # TODO: Why doesn't this function use Vertex objects? It's the only function which has _separated_ x and y lists + if len(x) != len(y): + raise FEMException( + "Wrong parameters", "x and y should have the same length." + ) + length = np.ones(len(x)) if EA is None: - EA = a * self.EA + EA_arr = length * self.EA + elif isinstance(EA, (float, int)): + EA_arr = length * EA + elif isinstance(EA, (list, np.ndarray)): + EA_arr = np.array(EA) + else: + raise FEMException( + "Wrong parameters", "EA should be a float, list or numpy array." + ) if EI is None: - EI = a * self.EI + EI_arr = length * self.EI + elif isinstance(EI, (float, int)): + EI_arr = length * EI + elif isinstance(EI, (list, np.ndarray)): + EI_arr = np.array(EI) + else: + raise FEMException( + "Wrong parameters", "EI should be a float, list or numpy array." + ) if g is None: - g = a * 0 - # cast to array if parameters are not given as array. - EA_arr = EA * a - EI_arr = EI * a - g_arr = g * a + g_arr = length * 0 + elif isinstance(g, (float, int)): + g_arr = length * g + elif isinstance(g, (list, np.ndarray)): + g_arr = np.array(g) + else: + raise FEMException( + "Wrong parameters", "g should be a float, list or numpy array." + ) for i in range(len(x) - 1): self.add_element( @@ -221,15 +257,8 @@ def add_truss_element( EA: Optional[float] = None, **kwargs: dict, ) -> int: - """ - .. highlight:: python - - Add an element that only has axial force. - - :param location: The two nodes of the element or the next node of the element. - - :Example: - + """Add a new truss element (an element that only has axial force) to the structure + Example: .. code-block:: python location=[[x, y], [x, y]] @@ -237,8 +266,12 @@ def add_truss_element( location=[x, y] location=Vertex - :param EA: EA - :return: Elements ID. + Args: + location (Union[ Sequence[Sequence[float]], Sequence[Vertex], Sequence[float], Vertex ]): The two nodes of the element or the next node of the element. + EA (Optional[float], optional): Axial stiffness of the new element. Defaults to None. + + Returns: + int: ID of the new element """ return self.add_element( location, @@ -259,50 +292,49 @@ def add_element( EA: Optional[float] = None, EI: Optional[float] = None, g: float = 0, - mp: Optional[MpType] = None, - spring: Optional[Spring] = None, + mp: Optional["MpType"] = None, + spring: Optional["Spring"] = None, **kwargs: Any, ) -> int: - """ - :param location: The two nodes of the element or the next node of the element. - - :Example: - + """Add a new general element (an element with axial and lateral force) to the structure + Example: .. code-block:: python - location=[[x, y], [x, y]] - location=[Vertex, Vertex] - location=[x, y] - location=Vertex - - :param EA: EA - :param EI: EI - :param g: Weight per meter. [kN/m] / [N/m] - :param mp: Set a maximum plastic moment capacity. Keys are integers representing - the nodes. Values are the bending moment capacity. - - :Example: - - .. code-block:: python - - mp={1: 210e3, - 2: 180e3} - - :param spring: Set a rotational spring or a hinge (k=0) at node 1 or node 2. + location=[[x, y], [x, y]] + location=[Vertex, Vertex] + location=[x, y] + location=Vertex - :Example: - - .. code-block:: python - - spring={1: k - 2: k} + mp={1: 210e3, 2: 180e3} + spring={1: k, 2: k} # Set a hinged node: spring={1: 0} - - :return: Elements ID. + Args: + location (Union[ Sequence[Sequence[float]], Sequence[Vertex], Sequence[float], Vertex ]): The two nodes of the element or the next node of the element + EA (Optional[float], optional): Axial stiffness of the new element. Defaults to None. + EI (Optional[float], optional): Bending stiffness of the new element. Defaults to None. + g (float, optional): Self-weight of the new element. Defaults to 0. + mp (Optional[MpType], optional): Maximum plastic moment of each end node. Keys are integers representing + the nodes. Values are the bending moment capacity. Defaults to None. + spring (Optional[Spring], optional): Rotational spring or hinge (k=0) of each end node. Keys are integers representing + the nodes. Values are the bending moment capacity. Defaults to None. + + Optional Keyword Args: + element_type (ElementType): "general" (axial and lateral force) or "truss" (axial force only) + steelsection (str): Steel section name like IPE 300 + orient (OrientAxis): Steel section axis for moment of inertia - 'y' and 'z' possible + b (float): Width of generic rectangle section + h (float): Height of generic rectangle section + d (float): Diameter of generic circle section + sw (bool): If true self weight of section is considered as dead load + E (float): Modulus of elasticity for section material + gamma (float): Weight of section material per volume unit. [kN/m3] / [N/m3]s + + Returns: + int: ID of the new element """ if mp is None: @@ -402,43 +434,46 @@ def add_multiple_elements( EA: Optional[float] = None, EI: Optional[float] = None, g: float = 0, - mp: Optional[MpType] = None, - spring: Optional[Spring] = None, + mp: Optional["MpType"] = None, + spring: Optional["Spring"] = None, **kwargs: Any, ) -> List[int]: - """ - Add multiple elements defined by the first and the last point. - - :param location: See 'add_element' method - :param n: Number of elements. - :param dl: Distance between the elements nodes. - :param EA: See 'add_element' method - :param EI: See 'add_element' method - :param g: See 'add_element' method - :param mp: See 'add_element' method - :param spring: See 'add_element' method - - **Keyword Args:** - - :param element_type: See 'add_element' method - :param first: Different arguments for the first element - :param last: Different arguments for the last element - :param steelsection: Steel section name like IPE 300 - :param orient: Steel section axis for moment of inertia - 'y' and 'z' possible - :param b: Width of generic rectangle section - :param h: Height of generic rectangle section - :param d: Diameter of generic circle section - :param sw: If true self weight of section is considered as dead load - :param E: Modulus of elasticity for section material - :param gamma: Weight of section material per volume unit. [kN/m3] / [N/m3]s - - :Example: + """Add multiple elements defined by the first and the last point. + + Example: .. code-block:: python last={'EA': 1e3, 'mp': 290} - :return: (list) Element IDs + Args: + location (Union[ Sequence[Sequence[float]], Sequence[Vertex], Sequence[float], Vertex ]): The two nodes of the element or the next node of the element. + n (Optional[int], optional): Number of elements to add between the first and last nodes. Defaults to None. + dl (Optional[float], optional): Length of sub-elements to add between the first and last nodes. Length will be rounded down if necessary such that all sub-elements will have the same length. Defaults to None. + EA (Optional[float], optional): Axial stiffness. Defaults to None. + EI (Optional[float], optional): Bending stiffness. Defaults to None. + g (float, optional): Self-weight. Defaults to 0. + mp (Optional[MpType], optional): Maximum plastic moment capacity at ends of element. Defaults to None. + spring (Optional[Spring], optional): Rotational springs or hinges (k=0) at ends of element. Defaults to None. + + Optional Keyword Args: + element_type (ElementType): "general" (axial and lateral force) or "truss" (axial force only) + first (dict): Different arguments for the first element + last (dict): Different arguments for the last element + steelsection (str): Steel section name like IPE 300 + orient (OrientAxis): Steel section axis for moment of inertia - 'y' and 'z' possible + b (float): Width of generic rectangle section + h (float): Height of generic rectangle section + d (float): Diameter of generic circle section + sw (bool): If true self weight of section is considered as dead load + E (float): Modulus of elasticity for section material + gamma (float): Weight of section material per volume unit. [kN/m3] / [N/m3]s + + Raises: + FEMException: One, and only one, of n and dl should be passed as argument. + + Returns: + List[int]: IDs of the new elements """ if mp is None: @@ -468,19 +503,18 @@ def add_multiple_elements( length = (point_2 - point_1).modulus() direction = (point_2 - point_1).unit() - if n is not None and dl is not None: - raise FEMException( - "Wrong parameters", - "One, and only one, of n and dl should be passed as argument.", - ) - if n: - var_n = n + if dl is None and n is not None: + var_n = np.ceil(n) lengths = np.linspace(start=0, stop=length, num=var_n + 1) - else: - assert dl is not None + elif dl is not None and n is None: var_n = int(np.ceil(length / dl) - 1) lengths = np.linspace(start=0, stop=var_n * dl, num=var_n + 1) lengths = np.append(lengths, length) + else: + raise FEMException( + "Wrong parameters", + "One, and only one, of n and dl should be passed as argument.", + ) point = point_1 + direction * lengths[1] elements = [ @@ -496,36 +530,43 @@ def add_multiple_elements( ) ] - for i in range(2, var_n): - point = point_1 + direction * lengths[i] + if var_n > 1: + for i in range(2, var_n): + point = point_1 + direction * lengths[i] + elements.append( + self.add_element( + point, + EA, + EI, + g, + mp, + spring, + element_type=element_type, + **kwargs, + ) + ) + elements.append( self.add_element( - point, EA, EI, g, mp, spring, element_type=element_type, **kwargs + point_2, + last["EA"], + last["EI"], + last["g"], + last["mp"], + last["spring"], + element_type=last["element_type"], + **kwargs, ) ) - - elements.append( - self.add_element( - point_2, - last["EA"], - last["EI"], - last["g"], - last["mp"], - last["spring"], - element_type=last["element_type"], - **kwargs, - ) - ) return elements def insert_node( self, element_id: int, - location: Union[Sequence[float], Vertex, None] = None, + location: Optional[Union[Sequence[float], Vertex]] = None, factor: Optional[float] = None, ) -> None: - """ - Insert a node into an existing structure. + """Insert a node into an existing structure. This can be done by adding a new Vertex at any given location, or by setting a factor of the elements length. E.g. if you want a node at 40% of the elements length, you pass factor = 0.4. @@ -533,18 +574,12 @@ def insert_node( Note: this method completely rebuilds the SystemElements object and is therefore slower then building a model with `add_element` methods. - :param element_id: Id number of the element you want to insert the node. - :param location: The nodes of the element or the next node of the element. - - :Example: - - .. code-block:: python - - location=[x, y] - location=Vertex - - :param: factor: Value between 0 and 1 to determine the new node location. + Args: + element_id (int): Id number of the element in which you want to insert the node + location (Optional[Union[Sequence[float], Vertex]], optional): Location in which to insert the node. Defaults to None. + factor (Optional[float], optional): Fraction of distance from start to end of elmeent on which to divide the element. Must be between 0 and 1. Defaults to None. """ + ss = SystemElements( EA=self.EA, EI=self.EI, load_factor=self.load_factor, mesh=self.plotter.mesh ) @@ -558,16 +593,29 @@ def insert_node( else {} ) if element_id == element.id: - if factor is not None: + if factor is not None and location is None: + if factor < 0 or factor > 1: + raise FEMException( + "Invalid factor parameter", + f"Factor should be between 0 and 1, but is {factor}", + ) location_vertex = Vertex( factor * (element.vertex_2 - element.vertex_1) + element.vertex_1 ) - else: + elif factor is None and location is not None: assert location is not None location_vertex = Vertex(location) + else: + raise FEMException( + "Invalid parameters", + "Either factor or location - but not both - must be passed as argument.", + ) - mp1 = mp2 = spring1 = spring2 = {} + mp1: "MpType" = {} + mp2: "MpType" = {} + spring1: "Spring" = {} + spring2: "Spring" = {} if len(mp) != 0: if 1 in mp: mp1 = {1: mp[1]} @@ -615,27 +663,26 @@ def solve( geometrical_non_linear: int = False, **kwargs: Any, ) -> np.ndarray: - """ - Compute the results of current model. + """Compute the results of current model. - :param force_linear: Force a linear calculation. Even when the system has non linear nodes. - :param verbosity: 0. Log calculation outputs. 1. silence. - :param max_iter: Maximum allowed iterations. - :param geometrical_non_linear: Calculate second order effects and determine the - buckling factor. - :return: Displacements vector. + Args: + force_linear (bool, optional): Force a linear calculation, even when the system has non-linear nodes. Defaults to False. + verbosity (int, optional): Log calculation outputs (0), or silence (1). Defaults to 0. + max_iter (int, optional): Maximum allowed iterations. Defaults to 200. + geometrical_non_linear (int, optional): Calculate second order effects and determine the buckling factor. Defaults to False. - - Development **kwargs: - :param naked: Whether or not to run the solve function without doing post processing. - :param discretize_kwargs: When doing a geometric non linear analysis you can reduce or + Optional Keyword Args: + naked (bool): Whether or not to run the solve function without doing post processing. + discretize_kwargs (dict): When doing a geometric non linear analysis you can reduce or increase the number of elements created that are used for determining the buckling_factor - """ - # kwargs: arguments for the iterative solver callers such as the _stiffness_adaptation - # method. - # naked (bool) Default = False, if True force lines won't be computed. + Raises: + FEMException: The eigenvalues of the stiffness matrix are non zero, which indicates a instable structure. Check your support conditions + + Returns: + np.ndarray: Displacements vector. + """ for node_id in self.node_map: system_components.util.check_internal_hinges(self, node_id) @@ -733,15 +780,18 @@ def solve( return self.system_displacement_vector def validate(self, min_eigen: float = 1e-9) -> bool: - """ - Validate the stability of the stiffness matrix. + """Validate the stability of the stiffness matrix. - :param min_eigen: Minimum value of the eigenvalues of the stiffness matrix. This value - should be close to zero. + Args: + min_eigen (float, optional): Minimum value of the eigenvalues of the stiffness matrix. This value should be close to zero. Defaults to 1e-9. + + Returns: + bool: True if the structure is stable, False if not. """ ss = copy.copy(self) system_components.assembly.prep_matrix_forces(ss) + assert ss.system_force_vector is not None assert ( np.abs(ss.system_force_vector).sum() != 0 ), "There are no forces on the structure" @@ -750,14 +800,15 @@ def validate(self, min_eigen: float = 1e-9) -> bool: system_components.assembly.process_conditions(ss) + assert ss.reduced_system_matrix is not None w, _ = np.linalg.eig(ss.reduced_system_matrix) return bool(np.all(w > min_eigen)) def add_support_hinged(self, node_id: Union[int, Sequence[int]]) -> None: - """ - Model a hinged support at a given node. + """Model a hinged support at a given node. - :param node_id: Represents the nodes ID + Args: + node_id (Union[int, Sequence[int]]): Represents the nodes ID """ if not isinstance(node_id, collections.abc.Iterable): node_id = [node_id] @@ -769,10 +820,10 @@ def add_support_hinged(self, node_id: Union[int, Sequence[int]]) -> None: self.supports_hinged.append(self.node_map[id_]) def add_support_rotational(self, node_id: Union[int, Sequence[int]]) -> None: - """ - Model a rotational support at a given node. + """Model a rotational support at a given node. - :param node_id: Represents the nodes ID + Args: + node_id (Union[int, Sequence[int]]): Represents the nodes ID """ if not isinstance(node_id, collections.abc.Iterable): node_id = [node_id] @@ -784,12 +835,12 @@ def add_support_rotational(self, node_id: Union[int, Sequence[int]]) -> None: self.supports_rotational.append(self.node_map[id_]) def add_internal_hinge(self, node_id: Union[int, Sequence[int]]) -> None: - """ - Model an internal hinge at a given node. - This may alternately be done by setting spring={n: 0} when creating elements - but this can be an easier method of doing so + """Model a internal hinge at a given node. + This may alternatively be done by setting a spring restraint to zero (`{1: 0}` or `{2: 0}`). + The effect is the same, though this function may be easier to use. - :param node_id: Represents the nodes ID + Args: + node_id (Union[int, Sequence[int]]): Represents the nodes ID """ if not isinstance(node_id, collections.abc.Iterable): node_id = [node_id] @@ -803,18 +854,20 @@ def add_internal_hinge(self, node_id: Union[int, Sequence[int]]) -> None: def add_support_roll( self, node_id: Union[Sequence[int], int], - direction: Union[Sequence[Union[str, int]], Union[str, int]] = "x", + direction: Union[Sequence["SupportDirection"], "SupportDirection"] = "x", angle: Union[Sequence[Optional[float]], Optional[float]] = None, rotate: Union[Sequence[bool], bool] = True, ) -> None: - """ - Adds a rolling support at a given node. + """Add a rolling support at a given node. + + Args: + node_id (Union[Sequence[int], int]): Represents the nodes ID + direction (Union[Sequence[SupportDirection], SupportDirection], optional): Represents the direction that is free ("x", "y", "1", or "2"). Defaults to "x". + angle (Union[Sequence[Optional[float]], Optional[float]], optional): Angle in degrees relative to global x-axis. If angle is given, the support will be inclined. Defaults to None. + rotate (Union[Sequence[bool], bool], optional): If set to False, rotation at the roller will also be restrianed. Defaults to True. - :param node_id: Represents the nodes ID - :param direction: Represents the direction that is free: 'x', 'y' - :param angle: Angle in degrees relative to global x-axis. - If angle is given, the support will be inclined. - :param rotate: If set to False, rotation at the roller will also be restrained. + Raises: + FEMException: Invalid direction, if the direction parameter is invalid """ if not isinstance(node_id, collections.abc.Iterable): node_id = [node_id] @@ -830,12 +883,15 @@ def add_support_roll( for id_, direction_, angle_, rotate_ in zip(node_id, direction, angle, rotate): id_ = _negative_index_to_id(id_, self.node_map.keys()) - if direction_ == "x": - direction_i = 2 - elif direction_ == "y": + if direction_ in ("x", "2", 2): + direction_i: Literal[1, 2] = 2 + elif direction_ in ("y", "1", 1): direction_i = 1 else: - direction_i = int(direction_) + raise FEMException( + "Invalid direction", + f"Direction should be 'x', 'y', '1' or '2', but is {direction_}", + ) if angle_ is not None: direction_i = 2 @@ -850,10 +906,10 @@ def add_support_fixed( self, node_id: Union[Sequence[int], int], ) -> None: - """ - Add a fixed support at a given node. + """Add a fixed support at a given node. - :param node_id: Represents the nodes ID + Args: + node_id (Union[Sequence[int], int]): Represents the nodes ID """ if not isinstance(node_id, collections.abc.Iterable): node_id = [ @@ -870,25 +926,18 @@ def add_support_fixed( def add_support_spring( self, node_id: Union[Sequence[int], int], - translation: Union[Sequence[int], int], + translation: Union[Sequence["AxisNumber"], "AxisNumber"], k: Union[Sequence[float], float], roll: Union[Sequence[bool], bool] = False, ) -> None: - """ - Add a translational support at a given node. - - :param translation: Represents the prevented translation. - - **Note** - - | 1 = translation in x - | 2 = translation in z - | 3 = rotation in y - - :param node_id: Integer representing the nodes ID. - :param k: Stiffness of the spring - :param roll: If set to True, only the translation of the spring is controlled. + """Add a spring support at a given node. + Args: + node_id (Union[Sequence[int], int]): Represents the nodes ID + translation (Union[Sequence[AxisNumber], AxisNumber]): Represents the prevented translation or rotation. + 1 = translation in x, 2 = translation in z, 3 = rotation in y + k (Union[Sequence[float], float]): Stiffness of the spring + roll (Union[Sequence[bool], bool], optional): If set to True, only the translation of the spring is controlled. Defaults to False. """ self.supports_spring_args.append((node_id, translation, k, roll)) # The stiffness of the spring is added in the system matrix at the location that @@ -926,18 +975,21 @@ def q_load( self, q: Union[float, Sequence[float]], element_id: Union[int, Sequence[int]], - direction: Union[str, Sequence[str]] = "element", + direction: Union["LoadDirection", Sequence["LoadDirection"]] = "element", rotation: Optional[Union[float, Sequence[float]]] = None, - q_perp: Union[float, Sequence[float]] = None, + q_perp: Optional[Union[float, Sequence[float]]] = None, ) -> None: - """ - Apply a q-load to an element. + """Apply a q-load (distributed load) to an element. - :param element_id: representing the element ID - :param q: value of the q-load - :param direction: "element", "x", "y", "parallel" - :param rotation: Rotate the force clockwise. Rotation is in degrees - :param q_perp: value of any q-load perpendicular to the indication direction/rotation + Args: + q (Union[float, Sequence[float]]): Value of the q-load + element_id (Union[int, Sequence[int]]): The element ID to which to apply the load + direction (Union["LoadDirection", Sequence["LoadDirection"]], optional): "element", "x", "y", "parallel", or "perpendicular". Defaults to "element". + rotation (Optional[Union[float, Sequence[float]]], optional): Rotate the force clockwise. Rotation is in degrees. Defaults to None. + q_perp (Optional[Union[float, Sequence[float]]], optional): Value of any q-load perpendicular to the indicated direction/rotatione. Defaults to None. + + Raises: + FEMException: _description_ """ q_arr: Sequence[Sequence[float]] q_perp_arr: Sequence[Sequence[float]] @@ -976,8 +1028,13 @@ def q_load( rotation[i] = np.pi / 2 elif direction[i] == "parallel": rotation[i] = self.element_map[element_id[i]].angle - else: + elif direction[i] in ("element", "perpendicular"): rotation[i] = np.pi / 2 + self.element_map[element_id[i]].angle + else: + raise FEMException( + "Invalid direction parameter", + "Direction should be 'x', 'y', 'parallel', 'perpendicular' or 'element'", + ) else: rotation[i] = math.radians(rotation[i]) direction[i] = "angle" @@ -1015,13 +1072,16 @@ def point_load( Fy: Union[float, Sequence[float]] = 0.0, rotation: Union[float, Sequence[float]] = 0.0, ) -> None: - """ - Apply a point load to a node. + """Apply a point load to a node. + + Args: + node_id (Union[int, Sequence[int]]): The node ID to which to apply the load + Fx (Union[float, Sequence[float]], optional): Force in the global X direction. Defaults to 0.0. + Fy (Union[float, Sequence[float]], optional): Force in the global Y direction. Defaults to 0.0. + rotation (Union[float, Sequence[float]], optional): Rotate the force clockwise by the given angle in degrees. Defaults to 0.0. - :param node_id: Nodes ID. - :param Fx: Force in global x direction. - :param Fy: Force in global x direction. - :param rotation: Rotate the force clockwise. Rotation is in degrees. + Raises: + FEMException: Point loads may not be placed at the location of inclined roller supports """ n = len(node_id) if isinstance(node_id, Sequence) else 1 node_id = arg_to_list(node_id, n) @@ -1052,11 +1112,11 @@ def point_load( def moment_load( self, node_id: Union[int, Sequence[int]], Ty: Union[float, Sequence[float]] ) -> None: - """ - Apply a moment on a node. + """Apply a moment load to a node. - :param node_id: Nodes ID. - :param Ty: Moments acting on the node. + Args: + node_id (Union[int, Sequence[int]]): The node ID to which to apply the load + Ty (Union[float, Sequence[float]]): Moment load (about the global Y direction) to apply """ n = len(node_id) if isinstance(node_id, Sequence) else 1 node_id = arg_to_list(node_id, n) @@ -1077,19 +1137,20 @@ def show_structure( values_only: bool = False, annotations: bool = False, ) -> Union[Tuple[np.ndarray, np.ndarray], Optional["Figure"]]: - """ - Plot the structure. - - :param factor: Influence the plotting scale. - :param verbosity: 0: All information, 1: Suppress information. - :param scale: Scale of the plot. - :param offset: Offset the plots location on the figure. - :param figsize: Change the figure size. - :param show: Plot the result or return a figure. - :param values_only: Return the values that would be plotted as tuple containing - two arrays: (x, y) - :param annotations: if True, structure annotations are plotted. It includes section name. - Note: only works when verbosity is equal to 0. + """Plot the structure. + + Args: + verbosity (int, optional): 0: All information, 1: Suppress information. Defaults to 0. + scale (float, optional): Scale of the plot. Defaults to 1.0. + offset (Tuple[float, float], optional): Offset the plots location on the figure. Defaults to (0, 0). + figsize (Optional[Tuple[float, float]], optional): Change the figure size. Defaults to None. + show (bool, optional): Plot the result or return a figure. Defaults to True. + supports (bool, optional): Plot the supports. Defaults to True. + values_only (bool, optional): Return the values that would be plotted as tuple containing two arrays: (x, y). Defaults to False. + annotations (bool, optional): if True, structure annotations are plotted. It includes section name. + + Returns: + Figure: If show is False, return a figure. """ figsize = self.figsize if figsize is None else figsize if values_only: @@ -1104,22 +1165,25 @@ def show_bending_moment( verbosity: int = 0, scale: float = 1, offset: Tuple[float, float] = (0, 0), - figsize: Tuple[float, float] = None, + figsize: Optional[Tuple[float, float]] = None, show: bool = True, values_only: bool = False, ) -> Union[Tuple[np.ndarray, np.ndarray], Optional["Figure"]]: + """Plot the bending moment. + + Args: + factor (Optional[float], optional): Influence the plotting scale. Defaults to None. + verbosity (int, optional): 0: All information, 1: Suppress information. Defaults to 0. + scale (float, optional): Scale of the plot. Defaults to 1. + offset (Tuple[float, float], optional): Offset the plots location on the figure. Defaults to (0, 0). + figsize (Optional[Tuple[float, float]], optional): Change the figure size. Defaults to None. + show (bool, optional): Plot the result or return a figure. Defaults to True. + values_only (bool, optional): Return the values that would be plotted as tuple containing two arrays: (x, y). Defaults to False. + + Returns: + Figure: If show is False, return a figure. """ - Plot the bending moment. - - :param factor: Influence the plotting scale. - :param verbosity: 0: All information, 1: Suppress information. - :param scale: Scale of the plot. - :param offset: Offset the plots location on the figure. - :param figsize: Change the figure size. - :param show: Plot the result or return a figure. - :param values_only: Return the values that would be plotted as tuple containing - two arrays: (x, y) - """ + if values_only: return self.plot_values.bending_moment(factor) figsize = self.figsize if figsize is None else figsize @@ -1137,17 +1201,19 @@ def show_axial_force( show: bool = True, values_only: bool = False, ) -> Union[Tuple[np.ndarray, np.ndarray], Optional["Figure"]]: - """ - Plot the axial force. - - :param factor: Influence the plotting scale. - :param verbosity: 0: All information, 1: Suppress information. - :param scale: Scale of the plot. - :param offset: Offset the plots location on the figure. - :param figsize: Change the figure size. - :param show: Plot the result or return a figure. - :param values_only: Return the values that would be plotted as tuple containing - two arrays: (x, y) + """Plot the axial force. + + Args: + factor (Optional[float], optional): Influence the plotting scale. Defaults to None. + verbosity (int, optional): 0: All information, 1: Suppress information. Defaults to 0. + scale (float, optional): Scale of the plot. Defaults to 1. + offset (Tuple[float, float], optional): Offset the plots location on the figure. Defaults to (0, 0). + figsize (Optional[Tuple[float, float]], optional): Change the figure size. Defaults to None. + show (bool, optional): Plot the result or return a figure. Defaults to True. + values_only (bool, optional): Return the values that would be plotted as tuple containing two arrays: (x, y). Defaults to False. + + Returns: + Figure: If show is False, return a figure. """ if values_only: return self.plot_values.axial_force(factor) @@ -1164,17 +1230,19 @@ def show_shear_force( show: bool = True, values_only: bool = False, ) -> Union[Tuple[np.ndarray, np.ndarray], Optional["Figure"]]: - """ - Plot the shear force. - - :param factor: Influence the plotting scale. - :param verbosity: 0: All information, 1: Suppress information. - :param scale: Scale of the plot. - :param offset: Offset the plots location on the figure. - :param figsize: Change the figure size. - :param show: Plot the result or return a figure. - :param values_only: Return the values that would be plotted as tuple containing - two arrays: (x, y) + """Plot the shear force. + + Args: + factor (Optional[float], optional): Influence the plotting scale. Defaults to None. + verbosity (int, optional): 0: All information, 1: Suppress information. Defaults to 0. + scale (float, optional): Scale of the plot. Defaults to 1. + offset (Tuple[float, float], optional): Offset the plots location on the figure. Defaults to (0, 0). + figsize (Optional[Tuple[float, float]], optional): Change the figure size. Defaults to None. + show (bool, optional): Plot the result or return a figure. Defaults to True. + values_only (bool, optional): Return the values that would be plotted as tuple containing two arrays: (x, y). Defaults to False. + + Returns: + Figure: If show is False, return a figure. """ if values_only: return self.plot_values.shear_force(factor) @@ -1189,14 +1257,17 @@ def show_reaction_force( figsize: Optional[Tuple[float, float]] = None, show: bool = True, ) -> Union[Tuple[np.ndarray, np.ndarray], Optional["Figure"]]: - """ - Plot the reaction force. + """Plot the reaction force. - :param verbosity: 0: All information, 1: Suppress information. - :param scale: Scale of the plot. - :param offset: Offset the plots location on the figure. - :param figsize: Change the figure size. - :param show: Plot the result or return a figure. + Args: + verbosity (int, optional): 0: All information, 1: Suppress information. Defaults to 0. + scale (float, optional): Scale of the plot. Defaults to 1. + offset (Tuple[float, float], optional): Offset the plots location on the figure. Defaults to (0, 0). + figsize (Optional[Tuple[float, float]], optional): Change the figure size. Defaults to None. + show (bool, optional): Plot the result or return a figure. Defaults to True. + + Returns: + Figure: If show is False, return a figure. """ figsize = self.figsize if figsize is None else figsize return self.plotter.reaction_force(figsize, verbosity, scale, offset, show) @@ -1212,18 +1283,20 @@ def show_displacement( linear: bool = False, values_only: bool = False, ) -> Union[Tuple[np.ndarray, np.ndarray], Optional["Figure"]]: - """ - Plot the displacement. - - :param factor: Influence the plotting scale. - :param verbosity: 0: All information, 1: Suppress information. - :param scale: Scale of the plot. - :param offset: Offset the plots location on the figure. - :param figsize: Change the figure size. - :param show: Plot the result or return a figure. - :param linear: Don't evaluate the displacement values in between the elements - :param values_only: Return the values that would be plotted as tuple containing - two arrays: (x, y) + """Plot the displacement. + + Args: + factor (Optional[float], optional): Influence the plotting scale. Defaults to None. + verbosity (int, optional): 0: All information, 1: Suppress information. Defaults to 0. + scale (float, optional): Scale of the plot. Defaults to 1. + offset (Tuple[float, float], optional): Offset the plots location on the figure. Defaults to (0, 0). + figsize (Optional[Tuple[float, float]], optional): Change the figure size. Defaults to None. + show (bool, optional): Plot the result or return a figure. Defaults to True. + linear (bool, optional): Don't evaluate the displacement values in between the elements. Defaults to False. + values_only (bool, optional): Return the values that would be plotted as tuple containing two arrays: (x, y). Defaults to False. + + Returns: + Figure: If show is False, return a figure. """ if values_only: return self.plot_values.displacements(factor, linear) @@ -1240,14 +1313,17 @@ def show_results( figsize: Optional[Tuple[float, float]] = None, show: bool = True, ) -> Optional["Figure"]: - """ - Plot all the results in one window. + """Plot all the results in one window. - :param verbosity: 0: All information, 1: Suppress information. - :param scale: Scale of the plot. - :param offset: Offset the plots location on the figure. - :param figsize: Change the figure size. - :param show: Plot the result or return a figure. + Args: + verbosity (int, optional): 0: All information, 1: Suppress information. Defaults to 0. + scale (float, optional): Scale of the plot. Defaults to 1. + offset (Tuple[float, float], optional): Offset the plots location on the figure. Defaults to (0, 0). + figsize (Optional[Tuple[float, float]], optional): Change the figure size. Defaults to None. + show (bool, optional): Plot the result or return a figure. Defaults to True. + + Returns: + Figure: If show is False, return a figure. """ figsize = self.figsize if figsize is None else figsize return self.plotter.results_plot(figsize, verbosity, scale, offset, show) @@ -1257,24 +1333,17 @@ def get_node_results_system( ) -> Union[ List[Tuple[Any, Any, Any, Any, Any, Any, Any]], Dict[str, Union[int, float]] ]: - """ - These are the node results. These are the opposite of the forces and displacements + """Get the node results. These are the opposite of the forces and displacements working on the elements and may seem counter intuitive. - :param node_id: representing the node's ID. If integer = 0, the results of all nodes - are returned - :return: - - | if node_id == 0: - | - | Returns a list containing tuples with the results: + Args: + node_id (int, optional): The node's ID. If node_id == 0, the results of all nodes are returned. Defaults to 0. - :: - - [(id, Fx, Fy, Ty, ux, uy, phi_y), (id, Fx, Fy...), () .. ] - | - | if node_id > 0: + Returns: + Union[ List[Tuple[Any, Any, Any, Any, Any, Any, Any]], Dict[str, Union[int, float]] ]: If node_id == 0, returns a list containing tuples with the results: [(id, Fx, Fy, Ty, ux, uy, phi_y), (id, Fx, Fy...), () .. ] + If node_id > 0, returns a dict with the results: {"id": id, "Fx": Fx, "Fy": Fy, "Ty": Ty, "ux": ux, "uy": uy, "phi_y": phi_y} """ + # TODO: This should return a List of Dicts, not a list of Tuples... result_list = [] if node_id != 0: node_id = _negative_index_to_id(node_id, self.node_map) @@ -1297,20 +1366,16 @@ def get_node_results_system( def get_node_displacements( self, node_id: int = 0 ) -> Union[List[Tuple[Any, Any, Any, Any]], Dict[str, Any]]: + """Get the node displacements. + + Args: + node_id (int, optional): The node's ID. If node_id == 0, the results of all nodes are returned. Defaults to 0. + + Returns: + Union[List[Tuple[Any, Any, Any, Any]], Dict[str, Any]]: If node_id == 0, returns a list containing tuples with the results: [(id, ux, uy, phi_y), (id, ux, uy, phi_y), ... (id, ux, uy, phi_y) ] + If node_id > 0, returns a dict with the results: {"id": id, "ux": ux, "uy": uy, "phi_y": phi_y} """ - :param node_id: Represents the node's ID. If integer = 0, the results of all nodes - are returned. - :return: - | if node_id == 0: - | - | Returns a list containing tuples with the results: - - :: - - [(id, ux, uy, phi_y), (id, ux, uy, phi_y), ... (id, ux, uy, phi_y) ] - | - | if node_id > 0: (dict) - """ + # TODO: This should return a List of Dicts, not a list of Tuples... result_list = [] if node_id != 0: node_id = _negative_index_to_id(node_id, self.node_map) @@ -1328,26 +1393,15 @@ def get_node_displacements( def get_element_results( self, element_id: int = 0, verbose: bool = False ) -> Union[List[Dict[str, Any]], Dict[str, Any]]: - """ - :param element_id: representing the elements ID. If elementID = 0 the results - of all elements are returned. - :param verbose: If set to True the numerical results for the deflection and the - bending moments are returned. + """Get the element results. - :return: - | - | if node_id == 0: - | - | Returns a list containing tuples with the results: - - :: - - [(id, length, alpha, u, N_1, N_2), (id, length, alpha, u, N_1, N_2), - ... (id, length, alpha, u, N_1, N_2)] - | - | if node_id > 0: (dict) - | + Args: + element_id (int, optional): The element's ID. If element_id == 0, the results of all elements are returned. Defaults to 0. + verbose (bool, optional): If set to True, then numerical results for the deflection and the bending moment are also returned. Defaults to False. + Returns: + Union[List[Dict[str, Any]], Dict[str, Any]]: If element_id == 0, returns a list containing dicts with the results: [{"id": id, "length": length, "alpha": alpha, "umax": umax, "umin": umin, "u": u, "wmax": wmax, "wmin": wmin, "w": w, "Mmin": Mmin, "Mmax": Mmax, "M": M, "Qmin": Qmin, "Qmax": Qmax, "Q": Q, "Nmin": Nmin, "Nmax": Nmax, "N": N, "q": q}, ... ] + If element_id > 0, returns a dict with the results: {"id": id, "length": length, "alpha": alpha, "umax": umax, "umin": umin, "u": u, "wmax": wmax, "wmin": wmin, "w": w, "Mmin": Mmin, "Mmax": Mmax, "M": M, "Qmin": Qmin, "Qmax": Qmax, "Q": Q, "Nmin": Nmin, "Nmax": Nmax, "N": N, "q": q} """ if element_id != 0: element_id = _negative_index_to_id(element_id, self.element_map) @@ -1443,32 +1497,52 @@ def get_element_results( ) return result_list - def get_element_result_range(self, unit: str) -> List[float]: - """ - Useful when added lots of elements. Returns a list of all the queried unit. + def get_element_result_range( + self, unit: Literal["shear", "moment", "axial"] + ) -> List[float]: + """Get the element results. Returns a list with the first mesh node results of each element for a certain unit. - :param unit: - - 'shear' - - 'moment' - - 'axial' + Args: + unit (str): "shear", "moment", or "axial" + Raises: + NotImplementedError: If the unit is not implemented. + + Returns: + List[float]: List with the first mesh node results of each element for a certain unit. """ + # TODO: This function does not make sense... Unclear why I should care about only the first node of each element if unit == "shear": - return [el.shear_force[0] for el in self.element_map.values()] + return [ + el.shear_force[0] + for el in self.element_map.values() + if el.shear_force is not None + ] if unit == "moment": - return [el.bending_moment[0] for el in self.element_map.values()] + return [ + el.bending_moment[0] + for el in self.element_map.values() + if el.bending_moment is not None + ] if unit == "axial": - return [el.axial_force[0] for el in self.element_map.values()] + return [ + el.axial_force[0] + for el in self.element_map.values() + if el.axial_force is not None + ] raise NotImplementedError - def get_node_result_range(self, unit: str) -> List[float]: - """ - Query a list with node results. + def get_node_result_range(self, unit: Literal["ux", "uy", "phi_y"]) -> List[float]: + """Get the node results. Returns a list with the node results for a certain unit. + + Args: + unit (str): "uy", "ux", or "phi_y" + + Raises: + NotImplementedError: If the unit is not implemented. - :param unit: - - 'uy' - - 'ux' - - 'phi_y' + Returns: + List[float]: List with the node results for a certain unit. """ if unit == "uy": return [node.uz for node in self.node_map.values()] # - * - = + @@ -1479,11 +1553,16 @@ def get_node_result_range(self, unit: str) -> List[float]: raise NotImplementedError def find_node_id(self, vertex: Union[Vertex, Sequence[float]]) -> Optional[int]: - """ - Retrieve the ID of a certain location. + """Find the ID of a certain location. + + Args: + vertex (Union[Vertex, Sequence[float]]): Vertex_xz, [x, y], (x, y) + + Raises: + TypeError: vertex must be a list, tuple or Vertex - :param vertex: Vertex_xz, [x, y], (x, y) - :return: id of the node at the location of the vertex + Returns: + Optional[int]: id of the node at the location of the vertex """ if isinstance(vertex, (list, tuple)): vertex_v = Vertex(vertex) @@ -1504,12 +1583,15 @@ def find_node_id(self, vertex: Union[Vertex, Sequence[float]]) -> Optional[int]: return None def nodes_range( - self, dimension: str + self, dimension: "Dimension" ) -> List[Union[float, Tuple[float, float], None]]: - """ - Retrieve a list with coordinates x or z (y). + """Retrieve a list with coordinates x or z (y). + + Args: + dimension (str): "both", 'x', 'y' or 'z' - :param dimension: "both", 'x', 'y' or 'z' + Returns: + List[Union[float, Tuple[float, float], None]]: List with coordinates x or z (y) """ return list( map( @@ -1527,14 +1609,16 @@ def nodes_range( ) def nearest_node( - self, dimension: str, val: Union[float, Sequence[float]] + self, dimension: "Dimension", val: Union[float, Sequence[float]] ) -> Union[int, None]: - """ - Retrieve the nearest node ID. + """Retrieve the nearest node ID. + + Args: + dimension (str): "both", 'x', 'y' or 'z' + val (Union[float, Sequence[float]]): Value of the dimension. - :param dimension: "both", 'x', 'y' or 'z' - :param val: Value of the dimension. - :return: ID of the node. + Returns: + Union[int, None]: ID of the node. """ if dimension == "both" and isinstance(val, Sequence): return int( @@ -1548,11 +1632,11 @@ def nearest_node( return int(np.argmin(np.abs(np.array(self.nodes_range(dimension)) - val))) def discretize(self, n: int = 10) -> None: - """ - Takes an already defined :class:`.SystemElements` object and increases the number + """Discretize the elements. Takes an already defined :class:`.SystemElements` object and increases the number of elements. - :param n: Divide the elements into n sub-elements. + Args: + n (int, optional): Divide the elements into n sub-elements.. Defaults to 10. """ ss = SystemElements( EA=self.EA, EI=self.EI, load_factor=self.load_factor, mesh=self.plotter.mesh @@ -1566,11 +1650,8 @@ def discretize(self, n: int = 10) -> None: else {} ) - for i, v in enumerate(vertex_range(element.vertex_1, element.vertex_2, n)): - if i == 0: - last_v = v - continue - + el_v_range = vertex_range(element.vertex_1, element.vertex_2, n) + for last_v, v in zip(el_v_range, el_v_range[1:]): loc = [last_v, v] ss.add_element( loc, @@ -1617,12 +1698,11 @@ def discretize(self, n: int = 10) -> None: self.__dict__ = ss.__dict__.copy() def remove_loads(self, dead_load: bool = False) -> None: - """ - Remove all the applied loads from the structure. + """Remove all the applied loads from the structure. - :param dead_load: Remove the dead load. + Args: + dead_load (bool, optional): Also remove the self-weights? Defaults to False. """ - self.loads_point = {} self.loads_q = {} self.loads_moment = {} @@ -1635,6 +1715,11 @@ def remove_loads(self, dead_load: bool = False) -> None: self.loads_dead_load = set() def apply_load_case(self, loadcase: LoadCase) -> None: + """Apply a load case to the structure. + + Args: + loadcase (LoadCase): Load case to apply. + """ for method, kwargs in loadcase.spec.items(): method = method.split("-")[0] kwargs = re.sub(r"[{}]", "", str(kwargs)) @@ -1644,6 +1729,14 @@ def apply_load_case(self, loadcase: LoadCase) -> None: exec(f"self.{method}({kwargs})") # pylint: disable=exec-used def __deepcopy__(self, _: str) -> "SystemElements": + """Deepcopy the SystemElements object. + + Args: + _ (str): Unnecessary argument. + + Returns: + SystemElements: Copied SystemElements object. + """ system = copy.copy(self) mesh = self.plotter.mesh system.plotter = None @@ -1659,6 +1752,18 @@ def __deepcopy__(self, _: str) -> "SystemElements": def _negative_index_to_id(idx: int, collection: Collection[int]) -> int: + """Convert a negative index to a positive index. (That is, allowing the Pythonic negative indexing) + + Args: + idx (int): Index to convert + collection (Collection[int]): Collection of indices to check against + + Raises: + TypeError: If the index is not an integer + + Returns: + int: Positive index + """ if not isinstance(idx, int): if int(idx) == idx: # if it can be non-destructively cast to an integer idx = int(idx) diff --git a/anastruct/fem/system_components/assembly.py b/anastruct/fem/system_components/assembly.py index 94a07ccf..f75c41a6 100644 --- a/anastruct/fem/system_components/assembly.py +++ b/anastruct/fem/system_components/assembly.py @@ -8,19 +8,21 @@ if TYPE_CHECKING: from anastruct.fem.elements import Element from anastruct.fem.system import SystemElements + from anastruct.types import AxisNumber def set_force_vector( - system: "SystemElements", force_list: List[Tuple[int, int, float]] + system: "SystemElements", force_list: List[Tuple[int, "AxisNumber", float]] ) -> np.ndarray: - """ - :param force_list: list containing tuples with the - 1. number of the node, - 2. the number of the direction (1 = x, 2 = z, 3 = y) - 3. the force - [(1, 3, 1000)] node=1, direction=3 (y), force=1000 - list may contain multiple tuples - :return: Vector with forces on the nodes + """Set a force vector on a node within the system + + Args: + system (SystemElements): System to which the force vector is applied + force_list (List[Tuple[int, AxisNumber, float]]): List of tuples containing + the node id, direction (1 = x, 2 = z, 3 = y), and force magnitude + + Returns: + np.ndarray: System force vector with the applied forces on the nodes """ assert system.system_force_vector is not None for id_, direction, force in force_list: @@ -30,6 +32,11 @@ def set_force_vector( def prep_matrix_forces(system: "SystemElements") -> None: + """Prepare the system force vector for the matrix assembly + + Args: + system (SystemElements): System to which the force vector is applied + """ system.system_force_vector = system.system_force_vector = np.zeros( len(system._vertices) * 3 ) @@ -40,11 +47,21 @@ def prep_matrix_forces(system: "SystemElements") -> None: def apply_moment_load(system: "SystemElements") -> None: + """Apply a moment load to the system + + Args: + system (SystemElements): System to which the moment load is applied + """ for node_id, Ty in system.loads_moment.items(): set_force_vector(system, [(node_id, 3, Ty)]) def apply_point_load(system: "SystemElements") -> None: + """Apply a point load to the system + + Args: + system (SystemElements): System to which the point load is applied + """ for node_id in system.loads_point: Fx, Fz = system.loads_point[node_id] # system force vector. @@ -58,6 +75,11 @@ def apply_point_load(system: "SystemElements") -> None: def apply_perpendicular_q_load(system: "SystemElements") -> None: + """Apply a perpendicular q load to the system + + Args: + system (SystemElements): System to which the perpendicular q load is applied + """ for element_id in system.loads_dead_load: element = system.element_map[element_id] qi_perpendicular = element.all_qp_load[0] @@ -108,6 +130,11 @@ def apply_perpendicular_q_load(system: "SystemElements") -> None: def apply_parallel_qn_load(system: "SystemElements") -> None: + """Apply a parallel qn load to the system + + Args: + system (SystemElements): System to which the parallel qn load is applied + """ for element_id in system.loads_dead_load: element = system.element_map[element_id] qni_parallel = element.all_qn_load[0] @@ -142,6 +169,13 @@ def apply_parallel_qn_load(system: "SystemElements") -> None: def dead_load(system: "SystemElements", g: float, element_id: int) -> None: + """Apply a dead load self-weight to an element in the system + + Args: + system (SystemElements): System to which the dead load is applied + g (float): Magnitude of the dead load self-weight + element_id (int): Element id to which the dead load is applied + """ system.loads_dead_load.add(element_id) system.element_map[element_id].dead_load = g @@ -149,9 +183,13 @@ def dead_load(system: "SystemElements", g: float, element_id: int) -> None: def assemble_system_matrix( system: "SystemElements", validate: bool = False, geometric_matrix: bool = False ) -> None: - """ - Shape of the matrix = n nodes * n d.o.f. - Shape = n * 3 + """Assemble the system matrix + Shape of the matrix = n nodes * n d.o.f. = n * 3 + + Args: + system (SystemElements): System to be prepared + validate (bool, optional): Whether or not to validate the system. Defaults to False. + geometric_matrix (bool, optional): Whether or not to include the current geometric matrix. Defaults to False. """ system._remainder_indexes = [] if not geometric_matrix: @@ -167,15 +205,15 @@ def assemble_system_matrix( # Determine the elements location in the stiffness matrix. # system matrix [K] # - # [fx 1] [K | \ node 1 starts at row 1 - # |fz 1] | K | / - # |Ty 1] | K | / - # |fx 2] | K | \ node 2 starts at row 4 - # |fz 2] | K | / - # |Ty 2] | K | / - # |fx 3] | K | \ node 3 starts at row 7 - # |fz 3] | K | / - # [Ty 3] [ K] / + # [fx 1] [K | \ node 1 starts at row 1 + # |fz 1] | K | / + # |Ty 1] | K | / + # |fx 2] | K | \ node 2 starts at row 4 + # |fz 2] | K | / + # |Ty 2] | K | / + # |fx 3] | K | \ node 3 starts at row 7 + # |fz 3] | K | / + # [Ty 3] [ K] / # # n n n # o o o @@ -204,15 +242,21 @@ def assemble_system_matrix( def set_displacement_vector( - system: "SystemElements", nodes_list: List[Tuple[int, int]] + system: "SystemElements", nodes_list: List[Tuple[int, "AxisNumber"]] ) -> np.ndarray: - """ - :param nodes_list: list containing tuples with - 1.the node - 2. the d.o.f. that is set - :return: Vector with the displacements of the nodes (If displacement is not known, - the value is set - to NaN) + """Set the displacement vector for the system + + Args: + system (SystemElements): System to which the displacement vector is applied + nodes_list (List[Tuple[int, AxisNumber]]): List of tuples containing + the node id and the direction (1 = x, 2 = z, 3 = y) + + Raises: + IndexError: This often occurs if you set supports before the all the elements are + modelled. First finish the model. + + Returns: + np.ndarray: System displacement vector with the applied displacements on the nodes """ if system.system_displacement_vector is None: system.system_displacement_vector = np.ones(len(system._vertices) * 3) * np.NaN @@ -232,6 +276,11 @@ def set_displacement_vector( def process_conditions(system: "SystemElements") -> None: + """Process the conditions of the system + + Args: + system (SystemElements): System to be processed + """ indexes = [] # remove the unsolvable values from the matrix and vectors assert system.shape_system_matrix is not None @@ -245,12 +294,19 @@ def process_conditions(system: "SystemElements") -> None: system.system_displacement_vector = np.delete( system.system_displacement_vector, indexes, 0 ) + assert system.system_force_vector is not None + assert system.system_matrix is not None system.reduced_force_vector = np.delete(system.system_force_vector, indexes, 0) system.reduced_system_matrix = np.delete(system.system_matrix, indexes, 0) system.reduced_system_matrix = np.delete(system.reduced_system_matrix, indexes, 1) def process_supports(system: "SystemElements") -> None: + """Process the supports of the system + + Args: + system (SystemElements): System to be processed + """ for node in system.supports_hinged: set_displacement_vector(system, [(node.id, 1), (node.id, 2)]) @@ -275,9 +331,7 @@ def process_supports(system: "SystemElements") -> None: for node in system.internal_hinges: set_displacement_vector(system, [(node.id, 3)]) for el in system.node_element_map[node.id]: - el.compile_constitutive_matrix( - el.EA, el.EI, el.l, el.springs, el.node_1.hinge, el.node_2.hinge - ) + el.compile_constitutive_matrix() el.compile_stiffness_matrix() for node in system.supports_fixed: @@ -302,5 +356,5 @@ def process_supports(system: "SystemElements") -> None: elif el.node_2.id == node_id: el.a2 = el.angle + angle - el.compile_kinematic_matrix(el.a1, el.a2, el.l) + el.compile_kinematic_matrix() el.compile_stiffness_matrix() diff --git a/anastruct/fem/system_components/solver.py b/anastruct/fem/system_components/solver.py index 41611692..d712d0fd 100644 --- a/anastruct/fem/system_components/solver.py +++ b/anastruct/fem/system_components/solver.py @@ -28,7 +28,8 @@ def stiffness_adaptation( mp > 0 for mpd in system.non_linear_elements.values() for mp in mpd ), "Cannot solve for an mp = 0. If you want a hinge set the spring stiffness equal to 0." - for c in range(max_iter): + iteration = 0 + while iteration < max_iter: factors = [] # update the elements stiffnesses @@ -63,13 +64,14 @@ def stiffness_adaptation( system.post_processor.reaction_forces() system.post_processor.element_results() break + iteration += 1 - if c == max_iter - 1: + if iteration >= max_iter: logging.warning( f"Couldn't solve the in the amount of iterations given. max_iter={max_iter}" ) elif verbosity == 0: - logging.info(f"Solved in {c} iterations") + logging.info(f"Solved in {iteration} iterations") assert system.system_displacement_vector is not None return system.system_displacement_vector diff --git a/anastruct/fem/system_components/util.py b/anastruct/fem/system_components/util.py index 4e36e699..f9df885a 100644 --- a/anastruct/fem/system_components/util.py +++ b/anastruct/fem/system_components/util.py @@ -1,22 +1,21 @@ -from typing import TYPE_CHECKING, Dict, Optional, Sequence, Tuple, Union +from typing import TYPE_CHECKING, Optional, Sequence, Tuple, Union from anastruct.basic import FEMException, angle_x_axis from anastruct.fem.node import Node from anastruct.vertex import Vertex if TYPE_CHECKING: - from anastruct.fem.system import Spring, SystemElements + from anastruct.fem.system import MpType, Spring, SystemElements def check_internal_hinges(system: "SystemElements", node_id: int) -> None: - """ - Identify internal hinges, set their hinge status + """Identify internal hinges, set their hinge status - Parameters - ---------- - node_id - Id of the node in the system + Args: + system (SystemElements): System in which the node is located + node_id (int): Id of the node in the system """ + node = system.node_map[node_id] hinges = [] @@ -61,6 +60,15 @@ def append_node_id( node_id1: int, node_id2: int, ) -> None: + """Append the node id to the system if it is not already in the system + + Args: + system (SystemElements): System in which the node is located + point_1 (Vertex): Vertex of the first point + point_2 (Vertex): Vertex of the second point + node_id1 (int): Node id of the first point + node_id2 (int): Node id of the second point + """ if node_id1 not in system.node_map: system.node_map[node_id1] = Node(node_id1, vertex=point_1) if node_id2 not in system.node_map: @@ -77,6 +85,20 @@ def det_vertices( Sequence[Sequence[float]], ], ) -> Tuple[Vertex, Vertex]: + """Determine the vertices of a location list + + Args: + system (SystemElements): System in which the nodes are located + location_list (Union[ Vertex, Sequence[Vertex], Sequence[int], Sequence[float], Sequence[Sequence[float]], ]): + List of one or two locations + + Raises: + FEMException: Raised when the location list is not a list of two points, a list of + two coordinates, or a list of two lists of two coordinates. + + Returns: + Tuple[Vertex, Vertex]: Tuple of two vertices + """ if isinstance(location_list, Vertex): point_1 = system._previous_point point_2 = Vertex(location_list) @@ -96,6 +118,12 @@ def det_vertices( ): point_1 = Vertex(location_list[0][0], location_list[0][1]) point_2 = Vertex(location_list[1][0], location_list[1][1]) + else: + raise FEMException( + "Flawed inputs", + "The location_list should be a list of two points, a list of two coordinates, " + + "or a list of two lists of two coordinates.", + ) system._previous_point = point_2 return point_1, point_2 @@ -104,6 +132,16 @@ def det_vertices( def det_node_ids( system: "SystemElements", point_1: Vertex, point_2: Vertex ) -> Tuple[int, int]: + """Determine the node ids of two points + + Args: + system (SystemElements): System in which the nodes are located + point_1 (Vertex): First point + point_2 (Vertex): Second point + + Returns: + Tuple[int, int]: Tuple of two node ids + """ node_ids = [] for p in (point_1, point_2): # k = str(p) @@ -118,6 +156,15 @@ def det_node_ids( def support_check(system: "SystemElements", node_id: int) -> None: + """Check if the node is a hinge + + Args: + system (SystemElements): System in which the node is located + node_id (int): Node id of the node + + Raises: + FEMException: Raised when the node is a hinge + """ if system.node_map[node_id].hinge: raise FEMException( "Flawed inputs", @@ -130,24 +177,27 @@ def force_elements_orientation( point_2: Vertex, node_id1: int, node_id2: int, - spring: Optional[Dict[int, float]], - mp: Optional[Dict[int, float]], -) -> Tuple[ - Vertex, - Vertex, - int, - int, - Optional[Dict[int, float]], - Optional[Dict[int, float]], - float, -]: - """ - Forces the elements to be in the first and the last quadrant of the unity circle. + spring: Optional["Spring"], + mp: Optional["MpType"], +) -> Tuple[Vertex, Vertex, int, int, Optional["Spring"], Optional["MpType"], float,]: + """Force the elements to be in the first and the last quadrant of the unity circle. Meaning the first node is always left and the last node is always right. Or they are both on one vertical line. The angle of the element will thus always be between -90 till +90 degrees. - :return: point_1, point_2, node_id1, node_id2, spring, mp, ai + + Args: + point_1 (Vertex): First point of the element + point_2 (Vertex): Second point of the element + node_id1 (int): Node id of the first point + node_id2 (int): Node id of the second point + spring (Optional[Spring]): Any spring releases of the element + mp (Optional[MpType]): Any maximum plastic moments of the element + + Returns: + Tuple[ Vertex, Vertex, int, int, Optional[Spring], Optional[MpType], float ]: + Tuple of the first point, second point, first node id, second node id, spring releases, + maximum plastic moments, and the angle of the element """ # determine the angle of the element with the global x-axis delta_x = point_2.x - point_1.x diff --git a/anastruct/fem/util/load.py b/anastruct/fem/util/load.py index 71f99752..a5ee5559 100644 --- a/anastruct/fem/util/load.py +++ b/anastruct/fem/util/load.py @@ -14,8 +14,10 @@ class LoadCase: """ def __init__(self, name: str): - """ - :param name: (str) Name of the load case + """Create a load case + + Args: + name (str): Name of the load case """ self.name: str = name self.spec: dict = {} diff --git a/anastruct/vertex.py b/anastruct/vertex.py index 431360df..9d335542 100644 --- a/anastruct/vertex.py +++ b/anastruct/vertex.py @@ -16,9 +16,11 @@ def __init__( x: Union[Vertex, Sequence[int], Sequence[float], np.ndarray, int, float], y: Union[int, float, None] = None, ): - """ - :param x: Can be any of int, float, coordinate list, or other vertex. - :param y: (int, flt) + """Create a Vertex object + + Args: + x (Union[Vertex, Sequence[int], Sequence[float], np.ndarray, int, float]): X coordinate + y (Union[int, float, None], optional): Y coordinate. Defaults to None. """ if isinstance(x, (Sequence)): self.coordinates: np.ndarray = np.array([x[0], x[1]], dtype=np.float32) @@ -31,25 +33,57 @@ def __init__( @property def x(self) -> float: + """X coordinate + + Returns: + float: X coordinate + """ return float(self.coordinates[0]) @property def y(self) -> float: + """Y coordinate + + Returns: + float: Y coordinate + """ return float(self.coordinates[1]) @property def z(self) -> float: + """Z coordinate (equals negative of Y coordinate) + + Returns: + float: Z coordinate + """ return float(self.coordinates[1] * -1) def modulus(self) -> float: + """Magnitude of the vector from the origin to the Vertex + + Returns: + float: Magnitude of the vector from the origin to the Vertex + """ return float(np.sqrt(np.sum(self.coordinates**2))) def unit(self) -> Vertex: + """Unit vector from the origin to the Vertex + + Returns: + Vertex: Unit vector from the origin to the Vertex + """ return (1 / self.modulus()) * self def displace_polar( self, alpha: float, radius: float, inverse_z_axis: bool = False ) -> None: + """Displace the Vertex by a polar coordinate + + Args: + alpha (float): Angle in radians + radius (float): Radius + inverse_z_axis (bool, optional): Return a Z coordinate (negative of Y coordinate)?. Defaults to False. + """ if inverse_z_axis: self.coordinates[0] += math.cos(alpha) * radius self.coordinates[1] -= math.sin(alpha) * radius @@ -58,6 +92,14 @@ def displace_polar( self.coordinates[1] += math.sin(alpha) * radius def __add__(self, other: Union[Vertex, tuple, list, np.ndarray, float]) -> Vertex: + """Add two Vertex objects + + Args: + other (Union[Vertex, tuple, list, np.ndarray, float]): Vertex to add + + Returns: + Vertex: Sum of the two Vertex objects + """ if isinstance(other, (tuple, list)): other = np.asarray(other) if isinstance(other, Vertex): @@ -67,9 +109,25 @@ def __add__(self, other: Union[Vertex, tuple, list, np.ndarray, float]) -> Verte return Vertex(coordinates) def __radd__(self, other: Union[Vertex, tuple, list, np.ndarray, float]) -> Vertex: + """Add two Vertex objects + + Args: + other (Union[Vertex, tuple, list, np.ndarray, float]): Vertex to add + + Returns: + Vertex: Sum of the two Vertex objects + """ return self.__add__(other) def __sub__(self, other: Union[Vertex, tuple, list, np.ndarray, float]) -> Vertex: + """Subtract two Vertex objects + + Args: + other (Union[Vertex, tuple, list, np.ndarray, float]): Vertex to subtract + + Returns: + Vertex: Difference of the two Vertex objects + """ if isinstance(other, (tuple, list)): other = np.asarray(other) if isinstance(other, Vertex): @@ -79,9 +137,25 @@ def __sub__(self, other: Union[Vertex, tuple, list, np.ndarray, float]) -> Verte return Vertex(coordinates) def __rsub__(self, other: Union[Vertex, tuple, list, np.ndarray, float]) -> Vertex: + """Subtract two Vertex objects + + Args: + other (Union[Vertex, tuple, list, np.ndarray, float]): Vertex to subtract + + Returns: + Vertex: Difference of the two Vertex objects + """ return self.__sub__(other) def __mul__(self, other: Union[Vertex, tuple, list, np.ndarray, float]) -> Vertex: + """Multiply two Vertex objects + + Args: + other (Union[Vertex, tuple, list, np.ndarray, float]): Vertex to multiply + + Returns: + Vertex: Product of the two Vertex objects + """ if isinstance(other, (tuple, list)): other = np.asarray(other) if isinstance(other, Vertex): @@ -91,11 +165,27 @@ def __mul__(self, other: Union[Vertex, tuple, list, np.ndarray, float]) -> Verte return Vertex(coordinates) def __rmul__(self, other: Union[Vertex, tuple, list, np.ndarray, float]) -> Vertex: + """Multiply two Vertex objects + + Args: + other (Union[Vertex, tuple, list, np.ndarray, float]): Vertex to multiply + + Returns: + Vertex: Product of the two Vertex objects + """ return self.__mul__(other) def __truediv__( self, other: Union[Vertex, tuple, list, np.ndarray, float] ) -> Vertex: + """Divide two Vertex objects + + Args: + other (Union[Vertex, tuple, list, np.ndarray, float]): Vertex to divide + + Returns: + Vertex: Quotient of the two Vertex objects + """ if isinstance(other, (tuple, list)): other = np.asarray(other) if isinstance(other, Vertex): @@ -105,17 +195,47 @@ def __truediv__( return Vertex(coordinates) def __eq__(self, other: object) -> bool: + """Check if two Vertex objects are equal + + Args: + other (object): Object to compare + + Returns: + bool: True if the two Vertex objects are equal + """ if isinstance(other, Vertex): return self.x == other.x and self.y == other.y + if isinstance(other, (tuple, list)) and len(other) == 2: + return self.x == other[0] and self.y == other[1] return NotImplemented def __str__(self) -> str: + """String representation of the Vertex object + + Returns: + str: String representation of the Vertex object + """ return f"Vertex({self.x}, {self.y})" def __hash__(self) -> int: + """Hash of the Vertex object + + Returns: + int: Hash of the Vertex object + """ return hash(str(self)) def vertex_range(v1: Vertex, v2: Vertex, n: int) -> list: + """Create a list of n Vertex objects between two Vertex objects + + Args: + v1 (Vertex): Starting Vertex + v2 (Vertex): Ending Vertex + n (int): Number of Vertex objects to create + + Returns: + list: List of n Vertex objects between v1 and v2 + """ dv = v2 - v1 return [v1 + dv * i / n for i in range(n + 1)] From 791b566eb18e5610d287f6d75c69fc960a5bee96 Mon Sep 17 00:00:00 2001 From: Brooks Smith Date: Tue, 10 Oct 2023 12:48:35 +1100 Subject: [PATCH 02/20] Add a types.py file to define arbitrary types for types that are used in more than one context, such as dimensions and axes --- anastruct/types.py | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 anastruct/types.py diff --git a/anastruct/types.py b/anastruct/types.py new file mode 100644 index 00000000..0e43c13e --- /dev/null +++ b/anastruct/types.py @@ -0,0 +1,10 @@ +from typing import Dict, Literal + +AxisNumber = Literal[1, 2, 3] +Dimension = Literal["x", "y", "z", "both"] +ElementType = Literal["general", "truss"] +LoadDirection = Literal["element", "x", "y", "parallel", "perpendicular", "angle"] +MpType = Dict[Literal[1, 2], float] +OrientAxis = Literal["y", "z"] +Spring = Dict[Literal[1, 2], float] +SupportDirection = Literal["x", "y", "1", "2", 1, 2] From d439539307783366bb561aeebe73704d604ccac8 Mon Sep 17 00:00:00 2001 From: Brooks Smith Date: Tue, 10 Oct 2023 12:49:30 +1100 Subject: [PATCH 03/20] Remove pylint disable of missing function docstring check --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1a9702b1..ada97b07 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -75,7 +75,6 @@ disable = [ "protected-access", "missing-module-docstring", "missing-class-docstring", - "missing-function-docstring", "too-many-lines", "duplicate-code", "invalid-name", From 16e6d24f2e51685c72cc3f5e0072fc2e284615be Mon Sep 17 00:00:00 2001 From: Brooks Smith Date: Tue, 10 Oct 2023 12:50:08 +1100 Subject: [PATCH 04/20] Enable basic pylance type checking too which picks up a few things that mypy does not, and is faster than mypy --- .vscode/settings.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index f63250c1..a070833d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -43,5 +43,6 @@ "mypy-type-checker.reportingScope": "workspace", "pylint.severity": { "refactor": "Information" - } + }, + "python.analysis.typeCheckingMode": "basic", } From b5ec1b83d4c23bbb98b9d4a774a5fd12fa935eb4 Mon Sep 17 00:00:00 2001 From: Brooks Smith Date: Tue, 10 Oct 2023 21:26:08 +1100 Subject: [PATCH 05/20] Docstring round 2 - all but plotter --- anastruct/fem/examples/ex_1_2.py | 1 + anastruct/fem/node.py | 54 +++++++++-- anastruct/fem/postprocess.py | 67 +++++++------ anastruct/fem/system.py | 135 +++++++++++++++++++-------- anastruct/material/profile.py | 8 ++ anastruct/material/units.py | 16 ++++ anastruct/sectionbase/properties.py | 18 ++++ anastruct/sectionbase/sectionbase.py | 48 +++++++++- 8 files changed, 269 insertions(+), 78 deletions(-) diff --git a/anastruct/fem/examples/ex_1_2.py b/anastruct/fem/examples/ex_1_2.py index 75375c75..52147632 100644 --- a/anastruct/fem/examples/ex_1_2.py +++ b/anastruct/fem/examples/ex_1_2.py @@ -2,6 +2,7 @@ def run() -> None: + """Run example 1.2""" system = se.SystemElements() system.add_element(location=[[3, 4], [0, 0]], EA=5e9, EI=8000) system.add_element(location=[[8, 4], [3, 4]], EA=5e9, EI=4000) diff --git a/anastruct/fem/node.py b/anastruct/fem/node.py index 7c7b92d6..556f3702 100644 --- a/anastruct/fem/node.py +++ b/anastruct/fem/node.py @@ -21,16 +21,18 @@ def __init__( vertex: Vertex = Vertex(0, 0), hinge: bool = False, ): - """ - :param id: ID of the node, integer - :param Fx: Value of Fx - :param Fz: Value of Fz - :param Ty: Value of Ty - :param ux: Value of ux - :param uz: Value of uz - :param phi_y: Value of phi - :param vertex: Point object - :param hinge: Boolean + """Create a Node object + + Args: + id (int): ID of the node + Fx (float, optional): Value of Fx force. Defaults to 0.0. + Fz (float, optional): Value of Fz force. Defaults to 0.0. + Ty (float, optional): Value of Ty moment. Defaults to 0.0. + ux (float, optional): Value of ux displacement. Defaults to 0.0. + uz (float, optional): Value of uz displacement. Defaults to 0.0. + phi_y (float, optional): Value of phi_y rotation. Defaults to 0.0. + vertex (Vertex, optional): Point object coordinate. Defaults to Vertex(0, 0). + hinge (bool, optional): Is this node a hinge. Defaults to False. """ self.id = id # forces @@ -47,9 +49,19 @@ def __init__( @property def Fy(self) -> float: + """Fy is the vertical force, and the negative of Fz + + Returns: + float: negative of Fz + """ return -self.Fz def __str__(self) -> str: + """String representation of the node + + Returns: + str: String representation of the node + """ if self.vertex: return ( f"[id = {self.id}, Fx = {self.Fx}, Fz = {self.Fz}, Ty = {self.Ty}, ux = {self.ux}, " @@ -61,6 +73,14 @@ def __str__(self) -> str: ) def __add__(self, other: Node) -> Node: + """Add two nodes + + Args: + other (Node): Node to add + + Returns: + Node: Sum of the two nodes + """ assert ( self.id == other.id ), "Cannot add nodes as the ID's don't match. The nodes positions don't match." @@ -81,6 +101,14 @@ def __add__(self, other: Node) -> Node: ) def __sub__(self, other: Node) -> Node: + """Subtract two nodes + + Args: + other (Node): Node to subtract + + Returns: + Node: Difference of the two nodes + """ assert ( self.id == other.id ), "Cannot subtract nodes as the ID's don't match. The nodes positions don't match." @@ -101,10 +129,16 @@ def __sub__(self, other: Node) -> Node: ) def reset(self) -> None: + """Reset the node to zero forces and displacements""" self.Fx = self.Fz = self.Ty = self.ux = self.uz = self.phi_y = 0 self.hinge = False def add_results(self, other: Node) -> None: + """Add the results of another node to this node + + Args: + other (Node): Node to add + """ assert ( self.id == other.id ), "Cannot add nodes as the ID's don't match. The nodes positions don't match." diff --git a/anastruct/fem/postprocess.py b/anastruct/fem/postprocess.py index aa3ebdeb..3732d11a 100644 --- a/anastruct/fem/postprocess.py +++ b/anastruct/fem/postprocess.py @@ -19,9 +19,8 @@ def __init__(self, system: "SystemElements"): self.post_el = ElementLevel(self.system) def node_results_elements(self) -> None: - """ - Determines the node results on the system level. - Results placed in SystemElements class: self.node_objects (list). + """Determines the node results on the element level. + Results placed in Element class: self.system.element_map[i].node_map (list). """ for el in self.system.element_map.values(): @@ -29,6 +28,9 @@ def node_results_elements(self) -> None: self.post_el.node_results(el) def node_results_system(self) -> None: + """Determines the node results on the system level. + Results place in SystemElements class: self.system.node_map (list) + """ for k, v in self.system.node_element_map.items(): # reset nodes in case of iterative calculation self.system.node_map[k].reset() @@ -55,6 +57,9 @@ def node_results_system(self) -> None: self.system.node_map[k].phi_y = -node.phi_y def reaction_forces(self) -> None: + """Determines the reaction forces on the system level. + Results place in SystemElements class: self.system.reaction_forces (list) + """ supports = [] for node in self.system.supports_fixed: supports.append(node.id) @@ -83,10 +88,7 @@ def reaction_forces(self) -> None: node.phi_y = 0.0 def element_results(self) -> None: - """ - Determines the element results for all elements in the system on element level. - """ - + """Determines the element results for all elements in the system on element level.""" for el in self.system.element_map.values(): con = self.system.plotter.mesh self.post_el.determine_axial_force(el, con) @@ -100,9 +102,7 @@ def __init__(self, system: "SystemElements"): self.system = system def node_results(self, element: "Element") -> None: - """ - Determine node results on the element level. - """ + """Determine node results on the element level.""" assert element.element_force_vector is not None assert element.element_primary_force_vector is not None @@ -160,6 +160,12 @@ def node_results(self, element: "Element") -> None: @staticmethod def determine_axial_force(element: "Element", con: int) -> None: + """Determines the axial force in the element. + + Args: + element (Element): Element for which to determine axial force + con (int): Number of points to determine axial force + """ N_1 = (math.sin(element.angle) * element.node_1.Fz) + -( math.cos(element.angle) * element.node_1.Fx ) @@ -185,6 +191,12 @@ def determine_axial_force(element: "Element", con: int) -> None: @staticmethod def determine_bending_moment(element: "Element", con: int) -> None: + """Determines the bending moment in the element. + + Args: + element (Element): Element for which to determine bending moment + con (int): + """ dT = -(element.node_2.Ty + element.node_1.Ty) # T2 - (-T1) iteration_factor = np.linspace(0, 1, con) @@ -204,9 +216,11 @@ def determine_bending_moment(element: "Element", con: int) -> None: @staticmethod def determine_shear_force(element: "Element", con: int) -> None: - """ - Determines the shear force by differentiating the bending moment. - :param element: (object) of the Element class + """Determines the shear force in the element, by differentiating the bending moment. + + Args: + element (Element): Element for which to determine shear force + con (int): Number of points to determine shear force """ iteration_factor = np.linspace(0, 1, con) x = iteration_factor * element.l @@ -217,24 +231,25 @@ def determine_shear_force(element: "Element", con: int) -> None: @staticmethod def determine_displacements(element: "Element", con: int) -> None: - """ - Determines the displacement by integrating the bending moment. - :param element: (object) of the Element class + """Determines the displacements in the element, by integrating the bending moment. - w = -M'' + w = -M'' - This gives you the formula + This gives you the formula - w = -aMx +bx + c + w = -aMx +bx + c - a = already defined by the integral - b = Scale the slope of the parabola. This is the rotation of the deflection. - You can think of this as the angle of the deflection beam. By rotating - the beam so that the last deflection w = 0 you get the correct - value for b. w[-1] = 0. - c = Translate the parabola. Translate it so that w[0] = 0 - """ + a = already defined by the integral + b = Scale the slope of the parabola. This is the rotation of the deflection. + You can think of this as the angle of the deflection beam. By rotating + the beam so that the last deflection w = 0 you get the correct + value for b. w[-1] = 0. + c = Translate the parabola. Translate it so that w[0] = 0 + Args: + element (Element): Element for which to determine displacements + con (int): Number of points to determine displacements + """ if element.type == "general": assert element.bending_moment is not None dx = element.l / (len(element.bending_moment) - 1) diff --git a/anastruct/fem/system.py b/anastruct/fem/system.py index 20339ae1..8b79df08 100644 --- a/anastruct/fem/system.py +++ b/anastruct/fem/system.py @@ -2,8 +2,19 @@ import copy import math import re -from typing import (TYPE_CHECKING, Any, Collection, Dict, List, Literal, - Optional, Sequence, Set, Tuple, Union) +from typing import ( + TYPE_CHECKING, + Any, + Collection, + Dict, + List, + Literal, + Optional, + Sequence, + Set, + Tuple, + Union, +) import numpy as np @@ -21,8 +32,14 @@ from matplotlib.figure import Figure from anastruct.fem.node import Node - from anastruct.types import (AxisNumber, Dimension, LoadDirection, MpType, - Spring, SupportDirection) + from anastruct.types import ( + AxisNumber, + Dimension, + LoadDirection, + MpType, + Spring, + SupportDirection, + ) class SystemElements: @@ -110,7 +127,7 @@ def __init__( self.supports_spring_x: List[Tuple[Node, bool]] = [] self.supports_spring_z: List[Tuple[Node, bool]] = [] self.supports_spring_y: List[Tuple[Node, bool]] = [] - self.supports_roll_direction: List[Literal[1, 2, 3]] = [] + self.supports_roll_direction: List[Literal[1, 2]] = [] self.inclined_roll: Dict[ int, float ] = {} # map node ids to inclination angle relative to global x-axis. @@ -267,7 +284,8 @@ def add_truss_element( location=Vertex Args: - location (Union[ Sequence[Sequence[float]], Sequence[Vertex], Sequence[float], Vertex ]): The two nodes of the element or the next node of the element. + location (Union[ Sequence[Sequence[float]], Sequence[Vertex], Sequence[float], Vertex ]): + The two nodes of the element or the next node of the element. EA (Optional[float], optional): Axial stiffness of the new element. Defaults to None. Returns: @@ -313,14 +331,15 @@ def add_element( spring={1: 0} Args: - location (Union[ Sequence[Sequence[float]], Sequence[Vertex], Sequence[float], Vertex ]): The two nodes of the element or the next node of the element + location (Union[ Sequence[Sequence[float]], Sequence[Vertex], Sequence[float], Vertex ]): + The two nodes of the element or the next node of the element EA (Optional[float], optional): Axial stiffness of the new element. Defaults to None. EI (Optional[float], optional): Bending stiffness of the new element. Defaults to None. g (float, optional): Self-weight of the new element. Defaults to 0. mp (Optional[MpType], optional): Maximum plastic moment of each end node. Keys are integers representing the nodes. Values are the bending moment capacity. Defaults to None. - spring (Optional[Spring], optional): Rotational spring or hinge (k=0) of each end node. Keys are integers representing - the nodes. Values are the bending moment capacity. Defaults to None. + spring (Optional[Spring], optional): Rotational spring or hinge (k=0) of each end node. + Keys are integers representing the nodes. Values are the bending moment capacity. Defaults to None. Optional Keyword Args: element_type (ElementType): "general" (axial and lateral force) or "truss" (axial force only) @@ -447,14 +466,18 @@ def add_multiple_elements( last={'EA': 1e3, 'mp': 290} Args: - location (Union[ Sequence[Sequence[float]], Sequence[Vertex], Sequence[float], Vertex ]): The two nodes of the element or the next node of the element. + location (Union[ Sequence[Sequence[float]], Sequence[Vertex], Sequence[float], Vertex ]): + The two nodes of the element or the next node of the element. n (Optional[int], optional): Number of elements to add between the first and last nodes. Defaults to None. - dl (Optional[float], optional): Length of sub-elements to add between the first and last nodes. Length will be rounded down if necessary such that all sub-elements will have the same length. Defaults to None. + dl (Optional[float], optional): Length of sub-elements to add between the first and last nodes. + Length will be rounded down if necessary such that all sub-elements will have the same length. + Defaults to None. EA (Optional[float], optional): Axial stiffness. Defaults to None. EI (Optional[float], optional): Bending stiffness. Defaults to None. g (float, optional): Self-weight. Defaults to 0. mp (Optional[MpType], optional): Maximum plastic moment capacity at ends of element. Defaults to None. - spring (Optional[Spring], optional): Rotational springs or hinges (k=0) at ends of element. Defaults to None. + spring (Optional[Spring], optional): Rotational springs or hinges (k=0) at ends of element. + Defaults to None. Optional Keyword Args: element_type (ElementType): "general" (axial and lateral force) or "truss" (axial force only) @@ -576,8 +599,10 @@ def insert_node( Args: element_id (int): Id number of the element in which you want to insert the node - location (Optional[Union[Sequence[float], Vertex]], optional): Location in which to insert the node. Defaults to None. - factor (Optional[float], optional): Fraction of distance from start to end of elmeent on which to divide the element. Must be between 0 and 1. Defaults to None. + location (Optional[Union[Sequence[float], Vertex]], optional): Location in which to insert the node. + Defaults to None. + factor (Optional[float], optional): Fraction of distance from start to end of elmeent on which to + divide the element. Must be between 0 and 1. Defaults to None. """ ss = SystemElements( @@ -666,10 +691,12 @@ def solve( """Compute the results of current model. Args: - force_linear (bool, optional): Force a linear calculation, even when the system has non-linear nodes. Defaults to False. + force_linear (bool, optional): Force a linear calculation, even when the system has non-linear nodes. + Defaults to False. verbosity (int, optional): Log calculation outputs (0), or silence (1). Defaults to 0. max_iter (int, optional): Maximum allowed iterations. Defaults to 200. - geometrical_non_linear (int, optional): Calculate second order effects and determine the buckling factor. Defaults to False. + geometrical_non_linear (int, optional): Calculate second order effects and determine the buckling factor. + Defaults to False. Optional Keyword Args: naked (bool): Whether or not to run the solve function without doing post processing. @@ -678,7 +705,8 @@ def solve( determining the buckling_factor Raises: - FEMException: The eigenvalues of the stiffness matrix are non zero, which indicates a instable structure. Check your support conditions + FEMException: The eigenvalues of the stiffness matrix are non zero, which indicates an unstable structure. + Check your support conditions Returns: np.ndarray: Displacements vector. @@ -783,7 +811,8 @@ def validate(self, min_eigen: float = 1e-9) -> bool: """Validate the stability of the stiffness matrix. Args: - min_eigen (float, optional): Minimum value of the eigenvalues of the stiffness matrix. This value should be close to zero. Defaults to 1e-9. + min_eigen (float, optional): Minimum value of the eigenvalues of the stiffness matrix. This value + should be close to zero. Defaults to 1e-9. Returns: bool: True if the structure is stable, False if not. @@ -862,9 +891,12 @@ def add_support_roll( Args: node_id (Union[Sequence[int], int]): Represents the nodes ID - direction (Union[Sequence[SupportDirection], SupportDirection], optional): Represents the direction that is free ("x", "y", "1", or "2"). Defaults to "x". - angle (Union[Sequence[Optional[float]], Optional[float]], optional): Angle in degrees relative to global x-axis. If angle is given, the support will be inclined. Defaults to None. - rotate (Union[Sequence[bool], bool], optional): If set to False, rotation at the roller will also be restrianed. Defaults to True. + direction (Union[Sequence[SupportDirection], SupportDirection], optional): Represents the direction + that is free ("x", "y", "1", or "2"). Defaults to "x". + angle (Union[Sequence[Optional[float]], Optional[float]], optional): Angle in degrees relative to + global x-axis. If angle is given, the support will be inclined. Defaults to None. + rotate (Union[Sequence[bool], bool], optional): If set to False, rotation at the roller will also + be restrianed. Defaults to True. Raises: FEMException: Invalid direction, if the direction parameter is invalid @@ -937,7 +969,8 @@ def add_support_spring( translation (Union[Sequence[AxisNumber], AxisNumber]): Represents the prevented translation or rotation. 1 = translation in x, 2 = translation in z, 3 = rotation in y k (Union[Sequence[float], float]): Stiffness of the spring - roll (Union[Sequence[bool], bool], optional): If set to True, only the translation of the spring is controlled. Defaults to False. + roll (Union[Sequence[bool], bool], optional): If set to True, only the translation of the + spring is controlled. Defaults to False. """ self.supports_spring_args.append((node_id, translation, k, roll)) # The stiffness of the spring is added in the system matrix at the location that @@ -984,9 +1017,12 @@ def q_load( Args: q (Union[float, Sequence[float]]): Value of the q-load element_id (Union[int, Sequence[int]]): The element ID to which to apply the load - direction (Union["LoadDirection", Sequence["LoadDirection"]], optional): "element", "x", "y", "parallel", or "perpendicular". Defaults to "element". - rotation (Optional[Union[float, Sequence[float]]], optional): Rotate the force clockwise. Rotation is in degrees. Defaults to None. - q_perp (Optional[Union[float, Sequence[float]]], optional): Value of any q-load perpendicular to the indicated direction/rotatione. Defaults to None. + direction (Union["LoadDirection", Sequence["LoadDirection"]], optional): + "element", "x", "y", "parallel", or "perpendicular". Defaults to "element". + rotation (Optional[Union[float, Sequence[float]]], optional): Rotate the force clockwise. + Rotation is in degrees. Defaults to None. + q_perp (Optional[Union[float, Sequence[float]]], optional): Value of any q-load perpendicular + to the indicated direction/rotatione. Defaults to None. Raises: FEMException: _description_ @@ -1078,7 +1114,8 @@ def point_load( node_id (Union[int, Sequence[int]]): The node ID to which to apply the load Fx (Union[float, Sequence[float]], optional): Force in the global X direction. Defaults to 0.0. Fy (Union[float, Sequence[float]], optional): Force in the global Y direction. Defaults to 0.0. - rotation (Union[float, Sequence[float]], optional): Rotate the force clockwise by the given angle in degrees. Defaults to 0.0. + rotation (Union[float, Sequence[float]], optional): Rotate the force clockwise by the given + angle in degrees. Defaults to 0.0. Raises: FEMException: Point loads may not be placed at the location of inclined roller supports @@ -1146,7 +1183,8 @@ def show_structure( figsize (Optional[Tuple[float, float]], optional): Change the figure size. Defaults to None. show (bool, optional): Plot the result or return a figure. Defaults to True. supports (bool, optional): Plot the supports. Defaults to True. - values_only (bool, optional): Return the values that would be plotted as tuple containing two arrays: (x, y). Defaults to False. + values_only (bool, optional): Return the values that would be plotted as tuple containing + two arrays: (x, y). Defaults to False. annotations (bool, optional): if True, structure annotations are plotted. It includes section name. Returns: @@ -1178,7 +1216,8 @@ def show_bending_moment( offset (Tuple[float, float], optional): Offset the plots location on the figure. Defaults to (0, 0). figsize (Optional[Tuple[float, float]], optional): Change the figure size. Defaults to None. show (bool, optional): Plot the result or return a figure. Defaults to True. - values_only (bool, optional): Return the values that would be plotted as tuple containing two arrays: (x, y). Defaults to False. + values_only (bool, optional): Return the values that would be plotted as tuple containing + two arrays: (x, y). Defaults to False. Returns: Figure: If show is False, return a figure. @@ -1210,7 +1249,8 @@ def show_axial_force( offset (Tuple[float, float], optional): Offset the plots location on the figure. Defaults to (0, 0). figsize (Optional[Tuple[float, float]], optional): Change the figure size. Defaults to None. show (bool, optional): Plot the result or return a figure. Defaults to True. - values_only (bool, optional): Return the values that would be plotted as tuple containing two arrays: (x, y). Defaults to False. + values_only (bool, optional): Return the values that would be plotted as tuple containing + two arrays: (x, y). Defaults to False. Returns: Figure: If show is False, return a figure. @@ -1239,7 +1279,8 @@ def show_shear_force( offset (Tuple[float, float], optional): Offset the plots location on the figure. Defaults to (0, 0). figsize (Optional[Tuple[float, float]], optional): Change the figure size. Defaults to None. show (bool, optional): Plot the result or return a figure. Defaults to True. - values_only (bool, optional): Return the values that would be plotted as tuple containing two arrays: (x, y). Defaults to False. + values_only (bool, optional): Return the values that would be plotted as tuple containing + two arrays: (x, y). Defaults to False. Returns: Figure: If show is False, return a figure. @@ -1293,7 +1334,8 @@ def show_displacement( figsize (Optional[Tuple[float, float]], optional): Change the figure size. Defaults to None. show (bool, optional): Plot the result or return a figure. Defaults to True. linear (bool, optional): Don't evaluate the displacement values in between the elements. Defaults to False. - values_only (bool, optional): Return the values that would be plotted as tuple containing two arrays: (x, y). Defaults to False. + values_only (bool, optional): Return the values that would be plotted as tuple containing + two arrays: (x, y). Defaults to False. Returns: Figure: If show is False, return a figure. @@ -1337,11 +1379,15 @@ def get_node_results_system( working on the elements and may seem counter intuitive. Args: - node_id (int, optional): The node's ID. If node_id == 0, the results of all nodes are returned. Defaults to 0. + node_id (int, optional): The node's ID. If node_id == 0, the results of all nodes are returned. + Defaults to 0. Returns: - Union[ List[Tuple[Any, Any, Any, Any, Any, Any, Any]], Dict[str, Union[int, float]] ]: If node_id == 0, returns a list containing tuples with the results: [(id, Fx, Fy, Ty, ux, uy, phi_y), (id, Fx, Fy...), () .. ] - If node_id > 0, returns a dict with the results: {"id": id, "Fx": Fx, "Fy": Fy, "Ty": Ty, "ux": ux, "uy": uy, "phi_y": phi_y} + Union[ List[Tuple[Any, Any, Any, Any, Any, Any, Any]], Dict[str, Union[int, float]] ]: + If node_id == 0, returns a list containing tuples with the results: + [(id, Fx, Fy, Ty, ux, uy, phi_y), (id, Fx, Fy...), () .. ] + If node_id > 0, returns a dict with the results: + {"id": id, "Fx": Fx, "Fy": Fy, "Ty": Ty, "ux": ux, "uy": uy, "phi_y": phi_y} """ # TODO: This should return a List of Dicts, not a list of Tuples... result_list = [] @@ -1369,10 +1415,12 @@ def get_node_displacements( """Get the node displacements. Args: - node_id (int, optional): The node's ID. If node_id == 0, the results of all nodes are returned. Defaults to 0. + node_id (int, optional): The node's ID. If node_id == 0, the results of all nodes are returned. + Defaults to 0. Returns: - Union[List[Tuple[Any, Any, Any, Any]], Dict[str, Any]]: If node_id == 0, returns a list containing tuples with the results: [(id, ux, uy, phi_y), (id, ux, uy, phi_y), ... (id, ux, uy, phi_y) ] + Union[List[Tuple[Any, Any, Any, Any]], Dict[str, Any]]: If node_id == 0, returns a list containing + tuples with the results: [(id, ux, uy, phi_y), (id, ux, uy, phi_y), ... (id, ux, uy, phi_y) ] If node_id > 0, returns a dict with the results: {"id": id, "ux": ux, "uy": uy, "phi_y": phi_y} """ # TODO: This should return a List of Dicts, not a list of Tuples... @@ -1396,12 +1444,19 @@ def get_element_results( """Get the element results. Args: - element_id (int, optional): The element's ID. If element_id == 0, the results of all elements are returned. Defaults to 0. - verbose (bool, optional): If set to True, then numerical results for the deflection and the bending moment are also returned. Defaults to False. + element_id (int, optional): The element's ID. If element_id == 0, the results of all elements are returned. + Defaults to 0. + verbose (bool, optional): If set to True, then numerical results for the deflection and the bending + moment are also returned. Defaults to False. Returns: - Union[List[Dict[str, Any]], Dict[str, Any]]: If element_id == 0, returns a list containing dicts with the results: [{"id": id, "length": length, "alpha": alpha, "umax": umax, "umin": umin, "u": u, "wmax": wmax, "wmin": wmin, "w": w, "Mmin": Mmin, "Mmax": Mmax, "M": M, "Qmin": Qmin, "Qmax": Qmax, "Q": Q, "Nmin": Nmin, "Nmax": Nmax, "N": N, "q": q}, ... ] - If element_id > 0, returns a dict with the results: {"id": id, "length": length, "alpha": alpha, "umax": umax, "umin": umin, "u": u, "wmax": wmax, "wmin": wmin, "w": w, "Mmin": Mmin, "Mmax": Mmax, "M": M, "Qmin": Qmin, "Qmax": Qmax, "Q": Q, "Nmin": Nmin, "Nmax": Nmax, "N": N, "q": q} + Union[List[Dict[str, Any]], Dict[str, Any]]: If element_id == 0, + returns a list containing dicts with the results: [{"id": id, "length": length, "alpha": alpha, + "umax": umax, "umin": umin, "u": u, "wmax": wmax, "wmin": wmin, "w": w, "Mmin": Mmin, "Mmax": Mmax, + "M": M, "Qmin": Qmin, "Qmax": Qmax, "Q": Q, "Nmin": Nmin, "Nmax": Nmax, "N": N, "q": q}, ... ] + If element_id > 0, returns a dict with the results: {"id": id, "length": length, "alpha": alpha, + "umax": umax, "umin": umin, "u": u, "wmax": wmax, "wmin": wmin, "w": w, "Mmin": Mmin, "Mmax": Mmax, + "M": M, "Qmin": Qmin, "Qmax": Qmax, "Q": Q, "Nmin": Nmin, "Nmax": Nmax, "N": N, "q": q} """ if element_id != 0: element_id = _negative_index_to_id(element_id, self.element_map) diff --git a/anastruct/material/profile.py b/anastruct/material/profile.py index 5d84552f..b0c97245 100644 --- a/anastruct/material/profile.py +++ b/anastruct/material/profile.py @@ -53,6 +53,14 @@ def load(st: str) -> Dict[int, dict]: + """Load profile data from string + + Args: + st (str): String containing profile data + + Returns: + Dict[int, dict]: Profile data + """ with io.StringIO(st) as f: r = csv.reader(f) profile = {} diff --git a/anastruct/material/units.py b/anastruct/material/units.py index be803850..2c91a110 100644 --- a/anastruct/material/units.py +++ b/anastruct/material/units.py @@ -8,8 +8,24 @@ def to_kN(v: float) -> float: + """Convert N to kN + + Args: + v (float): Value in N + + Returns: + float: Value in kN + """ return v / 1e3 def to_kNm2(v: float) -> float: + """Convert N/m^2 to kN/m^2 + + Args: + v (float): Value in N/m^2 + + Returns: + float: Value in kN/m^2 + """ return v * 1e-9 diff --git a/anastruct/sectionbase/properties.py b/anastruct/sectionbase/properties.py index a59d865d..13023f23 100644 --- a/anastruct/sectionbase/properties.py +++ b/anastruct/sectionbase/properties.py @@ -7,6 +7,14 @@ def steel_section_properties( **kwargs: Dict[str, Any] ) -> Tuple[str, float, float, float]: + """Get steel section properties + + Raises: + ValueError: Raised if orient is not defined + + Returns: + Tuple[str, float, float, float]: Section name, EA, EI, g + """ steel_section = kwargs.get("steelsection", "IPE 300") orient = kwargs.get("orient", "y") E = kwargs.get("E", 210e9) @@ -30,6 +38,11 @@ def steel_section_properties( def rectangle_properties(**kwargs: Dict[str, Any]) -> Tuple[str, float, float, float]: + """Get rectangle section properties + + Returns: + Tuple[str, float, float, float]: Section name, EA, EI, g + """ b = kwargs.get("b", 0.1) h = kwargs.get("h", 0.5) E = kwargs.get("E", 210e9) @@ -53,6 +66,11 @@ def rectangle_properties(**kwargs: Dict[str, Any]) -> Tuple[str, float, float, f def circle_properties(**kwargs: Dict[str, Any]) -> Tuple[str, float, float, float]: + """Get circle section properties + + Returns: + Tuple[str, float, float, float]: Section name, EA, EI, g + """ d = kwargs.get("d", 0.4) E = kwargs.get("E", 210e9) gamma = kwargs.get("gamma", 10000) diff --git a/anastruct/sectionbase/sectionbase.py b/anastruct/sectionbase/sectionbase.py index e6d38a46..bed34bc2 100644 --- a/anastruct/sectionbase/sectionbase.py +++ b/anastruct/sectionbase/sectionbase.py @@ -1,5 +1,5 @@ import os -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Literal, Optional from xml.etree import ElementTree from anastruct.sectionbase import units @@ -27,6 +27,11 @@ def __init__(self) -> None: @property def root(self) -> ElementTree.Element: + """Get root of xml tree + + Returns: + ElementTree.Element: Root of xml tree + """ if self._root is None: self.set_database_name("EU") self.load_data_from_xml() @@ -35,6 +40,11 @@ def root(self) -> ElementTree.Element: @property def available_sections(self) -> list: + """Get available sections + + Returns: + list: List of available sections + """ return list( map( lambda el: el.attrib["sectionname"], @@ -44,6 +54,11 @@ def available_sections(self) -> list: @property def available_units(self) -> Dict[str, List[str]]: + """Get available units + + Returns: + Dict[str, List[str]]: Dictionary of available units by unit type + """ return { "length": list(units.l_dict.keys()), "mass": list(units.m_dict.keys()), @@ -53,11 +68,23 @@ def available_units(self) -> Dict[str, List[str]]: def set_unit_system( self, length: str = "m", mass_unit: str = "kg", force_unit: str = "N" ) -> None: + """Set unit system + + Args: + length (str, optional): Length unit. Defaults to "m". + mass_unit (str, optional): Mass unit. Defaults to "kg". + force_unit (str, optional): Force unit. Defaults to "N". + """ self.current_length_unit = units.l_dict[length] self.current_mass_unit = units.m_dict[mass_unit] self.current_force_unit = units.f_dict[force_unit] - def set_database_name(self, basename: str) -> None: + def set_database_name(self, basename: Literal["EU", "UK", "US"]) -> None: + """Set database name + + Args: + basename (str): Database name + """ if basename in ("EU", "UK"): self.xml_length_unit = units.m self.xml_area_unit = units.m @@ -77,12 +104,21 @@ def set_database_name(self, basename: str) -> None: self.load_data_from_xml() def load_data_from_xml(self) -> None: + """Load data from xml file""" assert self.current_database is not None self._root = ElementTree.parse( os.path.join(os.path.dirname(__file__), "data", self.current_database) ).getroot() def get_section_parameters(self, section_name: str) -> dict: + """Get section parameters + + Args: + section_name (str): Section name + + Returns: + dict: Section parameters + """ if self.root is None: self.set_database_name("EU") self.load_data_from_xml() @@ -97,6 +133,14 @@ def get_section_parameters(self, section_name: str) -> dict: return element def convert_units(self, element: Dict[str, Any]) -> Dict[str, Any]: + """Convert units + + Args: + element (Dict[str, Any]): Element + + Returns: + Dict[str, Any]: Converted element + """ assert self.current_length_unit is not None assert self.current_mass_unit is not None assert self.current_force_unit is not None From 8ae2575996f65e1e7a651b573809959136921505 Mon Sep 17 00:00:00 2001 From: Brooks Smith Date: Tue, 10 Oct 2023 22:21:58 +1100 Subject: [PATCH 06/20] Miscellaneous typing fixes in plotter --- anastruct/fem/plotter/element.py | 4 ++++ anastruct/fem/plotter/values.py | 10 ++++++---- anastruct/vertex.py | 2 +- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/anastruct/fem/plotter/element.py b/anastruct/fem/plotter/element.py index bf468c87..5f3601e9 100644 --- a/anastruct/fem/plotter/element.py +++ b/anastruct/fem/plotter/element.py @@ -30,6 +30,7 @@ def plot_values_deflection( y2 = -element.vertex_2.z + uz2 if element.type == "general" and not linear: + assert element.deflection is not None n = len(element.deflection) x_val = np.linspace(x1, x2, n) y_val = np.linspace(y1, y2, n) @@ -106,6 +107,8 @@ def plot_values_axial_force( """ # Determine forces for horizontal element.angle = 0 + assert element.N_1 is not None + assert element.N_2 is not None N1 = element.N_1 N2 = element.N_2 @@ -150,6 +153,7 @@ def plot_values_shear_force( x2 = element.vertex_2.x y2 = -element.vertex_2.z + assert element.shear_force is not None n = len(element.shear_force) # apply angle ai diff --git a/anastruct/fem/plotter/values.py b/anastruct/fem/plotter/values.py index b2f26f15..0689d7bd 100644 --- a/anastruct/fem/plotter/values.py +++ b/anastruct/fem/plotter/values.py @@ -55,7 +55,7 @@ def displacements( max_displacement = max( map( lambda el: max( - abs(el.node_1.ux), abs(el.node_1.uz), el.max_deflection + abs(el.node_1.ux), abs(el.node_1.uz), el.max_deflection or 0 ) if el.type == "general" else 0, @@ -86,6 +86,7 @@ def bending_moment(self, factor: Optional[float]) -> Tuple[np.ndarray, np.ndarra ) factor = det_scaling_factor(max_moment, self.max_val_structure) + assert self.system.element_map[1].bending_moment is not None n = len(self.system.element_map[1].bending_moment) xy = np.hstack( [ @@ -102,14 +103,15 @@ def axial_force( max_force = max( map( lambda el: max( - abs(el.N_1), - abs(el.N_2), + 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]) / 2) * el.l), ), self.system.element_map.values(), ) ) factor = det_scaling_factor(max_force, self.max_val_structure) + assert self.system.element_map[1].axial_force is not None n = len(self.system.element_map[1].axial_force) xy = np.hstack( [ @@ -123,7 +125,7 @@ def shear_force(self, factor: Optional[float]) -> Tuple[np.ndarray, np.ndarray]: if factor is None: max_force = max( map( - lambda el: np.max(np.abs(el.shear_force)), + lambda el: np.max(np.abs(el.shear_force or 0.0)), self.system.element_map.values(), ) ) diff --git a/anastruct/vertex.py b/anastruct/vertex.py index 9d335542..cbad074d 100644 --- a/anastruct/vertex.py +++ b/anastruct/vertex.py @@ -194,7 +194,7 @@ def __truediv__( coordinates = self.coordinates / other return Vertex(coordinates) - def __eq__(self, other: object) -> bool: + def __eq__(self, other: object) -> bool | NotImplementedError: """Check if two Vertex objects are equal Args: From 2437887959fb76218a355b0e36b65065f7dc190c Mon Sep 17 00:00:00 2001 From: Brooks Smith Date: Tue, 10 Oct 2023 22:22:32 +1100 Subject: [PATCH 07/20] Restructure Plotter to track all axes, not just one_fig which also fixes a slew of typing issues --- anastruct/fem/plotter/mpl.py | 272 ++++++++++++++++++++++++----------- 1 file changed, 185 insertions(+), 87 deletions(-) diff --git a/anastruct/fem/plotter/mpl.py b/anastruct/fem/plotter/mpl.py index a9056f1a..2889e4e8 100644 --- a/anastruct/fem/plotter/mpl.py +++ b/anastruct/fem/plotter/mpl.py @@ -1,5 +1,5 @@ import math -from typing import TYPE_CHECKING, Optional, Sequence, Tuple +from typing import TYPE_CHECKING, List, Optional, Sequence, Tuple import matplotlib.patches as mpatches import matplotlib.pyplot as plt @@ -32,6 +32,7 @@ def __init__(self, system: "SystemElements", mesh: int): self.plot_values = PlottingValues(system, mesh) self.mesh: int = self.plot_values.mesh self.system: "SystemElements" = system + self.axes: List["Axes"] = [] self.one_fig: Optional["Axes"] = None self.max_q: float = 0 self.max_qn: float = 0 @@ -42,13 +43,16 @@ def __init__(self, system: "SystemElements", mesh: int): def max_val_structure(self) -> float: return self.plot_values.max_val_structure - def __start_plot(self, figsize: Optional[Tuple[float, float]]) -> None: + def __start_plot( + self, figsize: Optional[Tuple[float, float]] + ) -> Tuple[float, float]: plt.close("all") self.fig = plt.figure(figsize=figsize) - self.one_fig = self.fig.add_subplot(111) + self.axes = [self.fig.add_subplot(111)] plt.tight_layout() + return (self.fig.get_figwidth(), self.fig.get_figheight()) - def __fixed_support_patch(self, max_val: float) -> None: + def __fixed_support_patch(self, max_val: float, axes_i: int = 0) -> None: """ :param max_val: max scale of the plot """ @@ -61,9 +65,9 @@ def __fixed_support_patch(self, max_val: float) -> None: color="r", zorder=9, ) - self.one_fig.add_patch(support_patch) + self.axes[axes_i].add_patch(support_patch) - def __hinged_support_patch(self, max_val: float) -> None: + def __hinged_support_patch(self, max_val: float, axes_i: int = 0) -> None: """ :param max_val: max scale of the plot """ @@ -76,9 +80,9 @@ def __hinged_support_patch(self, max_val: float) -> None: color="r", zorder=9, ) - self.one_fig.add_patch(support_patch) + self.axes[axes_i].add_patch(support_patch) - def __rotational_support_patch(self, max_val: float) -> None: + def __rotational_support_patch(self, max_val: float, axes_i: int = 0) -> None: """ :param max_val: max scale of the plot """ @@ -92,9 +96,9 @@ def __rotational_support_patch(self, max_val: float) -> None: zorder=9, fill=False, ) - self.one_fig.add_patch(support_patch) + self.axes[axes_i].add_patch(support_patch) - def __roll_support_patch(self, max_val: float) -> None: + def __roll_support_patch(self, max_val: float, axes_i: int = 0) -> None: """ :param max_val: max scale of the plot """ @@ -119,9 +123,9 @@ def __roll_support_patch(self, max_val: float) -> None: 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 = plt.Polygon(triangle, color="r", zorder=9) - self.one_fig.add_patch(support_patch_poly) - self.one_fig.plot( + support_patch_poly = mpatches.Polygon(triangle, color="r", 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="r", @@ -136,7 +140,7 @@ def __roll_support_patch(self, max_val: float) -> None: zorder=9, fill=False, ) - self.one_fig.add_patch(rect_patch_regpoly) + self.axes[axes_i].add_patch(rect_patch_regpoly) elif direction == 2: # horizontal roll support_patch_regpoly = mpatches.RegularPolygon( @@ -146,9 +150,9 @@ def __roll_support_patch(self, max_val: float) -> None: color="r", zorder=9, ) - self.one_fig.add_patch(support_patch_regpoly) + self.axes[axes_i].add_patch(support_patch_regpoly) y = -node.vertex.z - 2 * radius - self.one_fig.plot( + self.axes[axes_i].plot( [node.vertex.x - radius, node.vertex.x + radius], [y, y], color="r" ) if not rotate: @@ -160,15 +164,15 @@ def __roll_support_patch(self, max_val: float) -> None: zorder=9, fill=False, ) - self.one_fig.add_patch(rect_patch_rect) + 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="r", zorder=9) - self.one_fig.add_patch(support_patch_poly) + self.axes[axes_i].add_patch(support_patch_poly) y = node.vertex.y - radius - self.one_fig.plot( + self.axes[axes_i].plot( [node.vertex.x + radius * 1.5, node.vertex.x + radius * 1.5], [y, y + 2 * radius], color="r", @@ -182,10 +186,10 @@ def __roll_support_patch(self, max_val: float) -> None: zorder=9, fill=False, ) - self.one_fig.add_patch(rect_patch_rect) + self.axes[axes_i].add_patch(rect_patch_rect) count += 1 - def __rotating_spring_support_patch(self, max_val: float) -> None: + def __rotating_spring_support_patch(self, max_val: float, axes_i: int = 0) -> None: """ :param max_val: max scale of the plot """ @@ -196,7 +200,7 @@ def __rotating_spring_support_patch(self, max_val: float) -> None: 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.one_fig.plot(x, y, color="r", zorder=9) + self.axes[axes_i].plot(x, y, color="r", zorder=9) # Triangle support_patch = mpatches.RegularPolygon( @@ -206,9 +210,9 @@ def __rotating_spring_support_patch(self, max_val: float) -> None: color="r", zorder=9, ) - self.one_fig.add_patch(support_patch) + self.axes[axes_i].add_patch(support_patch) - def __spring_support_patch(self, max_val: float) -> None: + def __spring_support_patch(self, max_val: float, axes_i: int = 0) -> None: """ :param max_val: max scale of the plot """ @@ -223,7 +227,7 @@ def __spring_support_patch(self, max_val: float) -> None: np.array([0, 0, left, right, left, right, left, 0, 0]) + node.vertex.x ) - self.one_fig.plot(xval, yval, color="r", zorder=10) + self.axes[axes_i].plot(xval, yval, color="r", zorder=10) # Triangle support_patch = mpatches.RegularPolygon( @@ -233,7 +237,7 @@ def __spring_support_patch(self, max_val: float) -> None: color="r", zorder=10, ) - self.one_fig.add_patch(support_patch) + self.axes[axes_i].add_patch(support_patch) for node, _ in self.system.supports_spring_x: xval = np.arange(0, 9, 1) * dh + node.vertex.x @@ -241,7 +245,7 @@ def __spring_support_patch(self, max_val: float) -> None: np.array([0, 0, left, right, left, right, left, 0, 0]) + node.vertex.y ) - self.one_fig.plot(xval, yval, color="r", zorder=10) + self.axes[axes_i].plot(xval, yval, color="r", zorder=10) # Triangle support_patch = mpatches.RegularPolygon( @@ -251,9 +255,9 @@ def __spring_support_patch(self, max_val: float) -> None: color="r", zorder=10, ) - self.one_fig.add_patch(support_patch) + self.axes[axes_i].add_patch(support_patch) - def __q_load_patch(self, max_val: float, verbosity: int) -> None: + def __q_load_patch(self, max_val: float, verbosity: int, axes_i: int = 0) -> None: """ :param max_val: max scale of the plot @@ -282,9 +286,9 @@ def __plot_patch( xn2 = x2 + np.sin(ai) * h2 * direction yn2 = y2 + np.cos(ai) * h2 * direction coordinates = ([x1, xn1, xn2, x2], [y1, yn1, yn2, y2]) - self.one_fig.plot(*coordinates, color="g") - rec = plt.Polygon(np.vstack(coordinates).T, color="g", alpha=0.3) - self.one_fig.add_patch(rec) + self.axes[axes_i].plot(*coordinates, color="g") + rec = mpatches.Polygon(np.vstack(coordinates).T, color="g", alpha=0.3) + self.axes[axes_i].add_patch(rec) if verbosity == 0: # arrow @@ -303,8 +307,12 @@ def __plot_patch( step_len_y = np.linspace(len_y1, len_y2, 11) average_h = (h1 + h2) / 2 # fc = face color, ec = edge color - self.one_fig.text(xn1, yn1, f"q={qi}", color="b", fontsize=9, zorder=10) - self.one_fig.text(xn2, yn2, f"q={q}", color="b", fontsize=9, zorder=10) + self.axes[axes_i].text( + xn1, yn1, f"q={qi}", color="b", fontsize=9, zorder=10 + ) + self.axes[axes_i].text( + xn2, yn2, f"q={q}", color="b", fontsize=9, zorder=10 + ) # add multiple arrows to fill load for counter, step_xi in enumerate(step_x): @@ -323,7 +331,7 @@ def __plot_patch( else: shape = "full" - self.one_fig.arrow( + self.axes[axes_i].arrow( step_xi, step_y[counter], step_len_x[counter], @@ -354,6 +362,7 @@ def __plot_patch( 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) @@ -399,7 +408,9 @@ def __arrow_patch_values( return x, y, len_x, len_y, F - def __point_load_patch(self, max_plot_range: float, verbosity: int = 0) -> None: + def __point_load_patch( + self, max_plot_range: float, verbosity: int = 0, axes_i: int = 0 + ) -> None: """ :param max_plot_range: max scale of the plot """ @@ -411,7 +422,7 @@ def __point_load_patch(self, max_plot_range: float, verbosity: int = 0) -> None: h = 0.1 * max_plot_range * F / self.max_system_point_load x, y, len_x, len_y, F = self.__arrow_patch_values(Fx, Fz, node, h) - self.one_fig.arrow( + self.axes[axes_i].arrow( x, y, len_x, @@ -423,14 +434,14 @@ def __point_load_patch(self, max_plot_range: float, verbosity: int = 0) -> None: zorder=11, ) if verbosity == 0: - self.one_fig.text(x, y, f"F={F}", color="k", fontsize=9, zorder=10) + self.axes[axes_i].text(x, y, f"F={F}", color="k", fontsize=9, zorder=10) - def __moment_load_patch(self, max_val: float) -> None: + def __moment_load_patch(self, max_val: float, axes_i: int = 0) -> None: h = 0.2 * max_val for k, v in self.system.loads_moment.items(): node = self.system.node_map[k] if v > 0: - self.one_fig.plot( + self.axes[axes_i].plot( node.vertex.x, -node.vertex.z, marker=r"$\circlearrowleft$", @@ -438,14 +449,14 @@ def __moment_load_patch(self, max_val: float) -> None: color="orange", ) else: - self.one_fig.plot( + self.axes[axes_i].plot( node.vertex.x, -node.vertex.z, marker=r"$\circlearrowright$", ms=25, color="orange", ) - self.one_fig.text( + self.axes[axes_i].text( node.vertex.x + h * 0.2, -node.vertex.z + h * 0.2, f"T={v}", @@ -464,6 +475,7 @@ def plot_structure( offset: Sequence[float] = (0, 0), gridplot: bool = False, annotations: bool = True, + axes_i: int = 0, ) -> Optional["Figure"]: """ :param show: (boolean) if True, plt.figure will plot. @@ -473,7 +485,10 @@ def plot_structure( :return: """ if not gridplot: - self.__start_plot(figsize) + 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() @@ -492,16 +507,16 @@ def plot_structure( minxrange = center_x - ax_range minyrange = center_y - ax_range * figsize[1] / figsize[0] - self.one_fig.axis((minxrange, plusxrange, minyrange, plusyrange)) + 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.one_fig.plot(x_val, y_val, color="black", marker="s") + self.axes[axes_i].plot(x_val, y_val, color="black", marker="s") if verbosity == 0: # add node ID to plot ax_range = max_plot_range * 0.015 - self.one_fig.text( + self.axes[axes_i].text( x_val[0] + ax_range, y_val[0] + ax_range, f"{el.node_id1}", @@ -509,7 +524,7 @@ def plot_structure( fontsize=9, zorder=10, ) - self.one_fig.text( + self.axes[axes_i].text( x_val[-1] + ax_range, y_val[-1] + ax_range, f"{el.node_id2}", @@ -523,7 +538,7 @@ def plot_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.one_fig.text( + self.axes[axes_i].text( x_scalar, y_scalar, str(el.id), color="r", fontsize=9, zorder=10 ) @@ -532,7 +547,7 @@ def plot_structure( if annotations: x_scalar += +np.sin(el.angle) * factor * 2.3 y_scalar += -np.cos(el.angle) * factor * 2.3 - self.one_fig.text( + self.axes[axes_i].text( x_scalar, y_scalar, el.section_name, @@ -568,11 +583,12 @@ def _add_node_values( value_1: float, value_2: float, digits: int, + axes_i: int = 0, ) -> None: offset = self.max_val_structure * 0.015 # add value to plot - self.one_fig.text( + self.axes[axes_i].text( x_val[1] - offset, y_val[1] + offset, f"{round(value_1, digits)}", @@ -580,7 +596,7 @@ def _add_node_values( ha="center", va="center", ) - self.one_fig.text( + self.axes[axes_i].text( x_val[-2] - offset, y_val[-2] + offset, f"{round(value_2, digits)}", @@ -596,8 +612,9 @@ def _add_element_values( value: float, index: int, digits: int = 2, + axes_i: int = 0, ) -> None: - self.one_fig.text( + self.axes[axes_i].text( x_val[index], y_val[index], f"{round(value, digits)}", @@ -615,17 +632,20 @@ def plot_result( node_results: bool = True, fill_polygon: bool = True, color: int = 0, + axes_i: int = 0, ) -> None: if fill_polygon: - rec = plt.Polygon(np.vstack(axis_values).T, color=f"C{color}", alpha=0.3) - self.one_fig.add_patch(rec) + rec = mpatches.Polygon( + np.vstack(axis_values).T, color=f"C{color}", alpha=0.3 + ) + self.axes[axes_i].add_patch(rec) # plot force x_val = axis_values[0] y_val = axis_values[1] - self.one_fig.plot(x_val, y_val, color=f"C{color}") + self.axes[axes_i].plot(x_val, y_val, color=f"C{color}") - if node_results: + if node_results and force_1 and force_2: self._add_node_values(x_val, y_val, force_1, force_2, digits) def plot(self) -> None: @@ -640,16 +660,20 @@ def axial_force( offset: Sequence[float] = (0, 0), show: bool = True, gridplot: bool = False, + axes_i: int = 0, ) -> Optional["Figure"]: - self.plot_structure(figsize, 1, scale=scale, offset=offset, gridplot=gridplot) + self.plot_structure( + figsize, 1, scale=scale, offset=offset, gridplot=gridplot, axes_i=axes_i + ) + assert self.system.element_map[1].axial_force is not None con = len(self.system.element_map[1].axial_force) if factor is None: max_force = max( map( lambda el: max( - abs(el.N_1), - abs(el.N_2), + 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(), @@ -658,6 +682,8 @@ def axial_force( 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) @@ -684,7 +710,7 @@ def axial_force( ) if verbosity == 0: - self.one_fig.text( + self.axes[axes_i].text( point.x, point.y, "-", @@ -701,7 +727,7 @@ def axial_force( ) if verbosity == 0: - self.one_fig.text( + self.axes[axes_i].text( point.x, point.y, "+", @@ -725,8 +751,12 @@ def bending_moment( offset: Sequence[float] = (0, 0), show: bool = True, gridplot: bool = False, + axes_i: int = 0, ) -> Optional["Figure"]: - self.plot_structure(figsize, 1, scale=scale, offset=offset, gridplot=gridplot) + self.plot_structure( + figsize, 1, scale=scale, offset=offset, gridplot=gridplot, axes_i=axes_i + ) + assert self.system.element_map[1].bending_moment is not None con = len(self.system.element_map[1].bending_moment) if factor is None: # maximum moment determined by comparing the node's moments and the sagging moments. @@ -764,6 +794,7 @@ def bending_moment( ) 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 @@ -771,7 +802,7 @@ def bending_moment( if verbosity == 0: x = axis_values[0][index] + np.sin(-el.angle) * offset1 y = axis_values[1][index] + np.cos(-el.angle) * offset1 - self.one_fig.text(x, y, f"{round(m_sag, 1)}", fontsize=9) + self.axes[axes_i].text(x, y, f"{round(m_sag, 1)}", fontsize=9) if show: self.plot() return None @@ -787,15 +818,16 @@ def shear_force( show: bool = True, gridplot: bool = False, include_structure: bool = True, + axes_i: int = 0, ) -> Optional["Figure"]: if include_structure: self.plot_structure( - figsize, 1, scale=scale, offset=offset, gridplot=gridplot + 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)), + lambda el: np.max(np.abs(el.shear_force or 0.0)), self.system.element_map.values(), ) ) @@ -812,6 +844,7 @@ def 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] @@ -831,9 +864,16 @@ def reaction_force( offset: Sequence[float], show: bool, gridplot: bool = False, + axes_i: int = 0, ) -> Optional["Figure"]: self.plot_structure( - figsize, 1, supports=False, scale=scale, offset=offset, gridplot=gridplot + figsize, + 1, + supports=False, + scale=scale, + offset=offset, + gridplot=gridplot, + axes_i=axes_i, ) h = 0.2 * self.max_val_structure @@ -854,7 +894,7 @@ def reaction_force( len_x = sol[2] len_y = sol[3] - self.one_fig.arrow( + self.axes[axes_i].arrow( x, y, len_x, @@ -867,7 +907,7 @@ def reaction_force( ) if verbosity == 0: - self.one_fig.text( + self.axes[axes_i].text( x, y, f"R={round(node.Fx, 2)}", @@ -885,7 +925,7 @@ def reaction_force( len_x = sol[2] len_y = sol[3] - self.one_fig.arrow( + self.axes[axes_i].arrow( x, y, len_x, @@ -898,7 +938,7 @@ def reaction_force( ) if verbosity == 0: - self.one_fig.text( + self.axes[axes_i].text( x, y, f"R={round(node.Fz, 2)}", @@ -910,7 +950,7 @@ def reaction_force( if not math.isclose(node.Ty, 0, rel_tol=1e-5, abs_tol=1e-9): # '$...$': render the strings using mathtext if node.Ty > 0: - self.one_fig.plot( + self.axes[axes_i].plot( node.vertex.x, -node.vertex.z, marker=r"$\circlearrowleft$", @@ -918,7 +958,7 @@ def reaction_force( color="orange", ) if node.Ty < 0: - self.one_fig.plot( + self.axes[axes_i].plot( node.vertex.x, -node.vertex.z, marker=r"$\circlearrowright$", @@ -927,7 +967,7 @@ def reaction_force( ) if verbosity == 0: - self.one_fig.text( + self.axes[axes_i].text( node.vertex.x + h * 0.2, -node.vertex.z + h * 0.2, f"T={round(node.Ty, 2)}", @@ -950,14 +990,19 @@ def displacements( # pylint: disable=arguments-renamed show: bool = True, linear: bool = False, gridplot: bool = False, + axes_i: int = 0, ) -> Optional["Figure"]: - self.plot_structure(figsize, 1, scale=scale, offset=offset, gridplot=gridplot) + 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: max( - abs(el.node_1.ux), abs(el.node_1.uz), el.max_deflection + abs(el.node_1.ux), + abs(el.node_1.uz), + el.max_deflection or 0, ) if el.type == "general" else 0, @@ -971,6 +1016,7 @@ def displacements( # pylint: disable=arguments-renamed self.plot_result(axis_values, node_results=False, fill_polygon=False) 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) @@ -1011,28 +1057,80 @@ def results_plot( :return: Figure or None """ plt.close("all") + self.axes = [] self.fig = plt.figure(figsize=figsize) a = 320 - self.one_fig = self.fig.add_subplot(a + 1) + 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 + figsize, + verbosity, + show=False, + scale=scale, + offset=offset, + gridplot=True, + axes_i=0, ) - self.one_fig = self.fig.add_subplot(a + 2) + self.axes.append(self.fig.add_subplot(a + 2)) plt.title("bending moment") - self.bending_moment(None, figsize, verbosity, scale, offset, False, True) - self.one_fig = self.fig.add_subplot(a + 3) + 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(None, figsize, verbosity, scale, offset, False, True) - self.one_fig = self.fig.add_subplot(a + 4) + 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(None, figsize, verbosity, scale, offset, False, True) - self.one_fig = self.fig.add_subplot(a + 5) + 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(None, figsize, verbosity, scale, offset, False, False, True) - self.one_fig = self.fig.add_subplot(a + 6) + 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, verbosity, scale, offset, False, True) + self.reaction_force( + figsize=figsize, + verbosity=verbosity, + scale=scale, + offset=offset, + show=False, + gridplot=True, + axes_i=5, + ) if show: self.plot() From 106a35785c9c56580ec7e7de4020e37d614edfd5 Mon Sep 17 00:00:00 2001 From: Brooks Smith Date: Tue, 24 Oct 2023 21:57:28 +1100 Subject: [PATCH 08/20] Fix bug in un-reversed extension array --- anastruct/fem/postprocess.py | 1 + 1 file changed, 1 insertion(+) diff --git a/anastruct/fem/postprocess.py b/anastruct/fem/postprocess.py index 3732d11a..433ba310 100644 --- a/anastruct/fem/postprocess.py +++ b/anastruct/fem/postprocess.py @@ -289,6 +289,7 @@ def determine_displacements(element: "Element", con: int) -> None: phi_neg2 = -integrate_array(element.axial_force[::-1], dx) / element.EA u2 = integrate_array(phi_neg2, dx) + u2 = u2[::-1] element.extension = -1 * (u1 + u2) / 2.0 element.max_extension = np.max(np.abs(element.extension)) From bd712f16f5bbeb278d05a69be5b0cde6b6b56085 Mon Sep 17 00:00:00 2001 From: Brooks Smith Date: Tue, 24 Oct 2023 22:45:53 +1100 Subject: [PATCH 09/20] Docstring plotter --- anastruct/fem/plotter/element.py | 60 +++++-- anastruct/fem/plotter/mpl.py | 264 ++++++++++++++++++++++++++----- anastruct/fem/plotter/values.py | 63 +++++++- 3 files changed, 326 insertions(+), 61 deletions(-) diff --git a/anastruct/fem/plotter/element.py b/anastruct/fem/plotter/element.py index 5f3601e9..9e167cf9 100644 --- a/anastruct/fem/plotter/element.py +++ b/anastruct/fem/plotter/element.py @@ -10,14 +10,15 @@ def plot_values_deflection( element: "Element", factor: float, linear: bool = False ) -> Tuple[np.ndarray, np.ndarray]: - """ - Determine the plotting values for deflection + """Determines the plotting values for deflection + + Args: + element (Element): Element to plot + factor (float): Factor by which to multiply the plotting values perpendicular to the elements axis. + linear (bool, optional): If True, the bending in between the elements is determined. Defaults to False. - :param element: (fem.Element) - :param factor: (flt) Factor by which to multiply the plotting values perpendicular - to the elements axis. - :param linear: (bool) If True, the bending in between the elements is determined. - :return: (np.array/ list) x and y values. + Returns: + Tuple[np.ndarray, np.ndarray]: x and y values """ ux1 = element.node_1.ux * factor uz1 = -element.node_1.uz * factor @@ -48,11 +49,15 @@ def plot_values_deflection( def plot_values_bending_moment( element: "Element", factor: float, n: int ) -> Tuple[np.ndarray, np.ndarray]: - """ - :param element: (object) of the Element class - :param factor: (float) scaling the plot - :param n: (integer) amount of x-values - :return: + """Determines the plotting values for bending moment + + Args: + element (Element): Element to plot + factor (float): Factor by which to multiply the plotting values perpendicular to the elements axis. + n (int): Number of points to plot + + Returns: + Tuple[np.ndarray, np.ndarray]: x and y values """ # Determine forces for horizontal element.angle = 0 @@ -99,11 +104,15 @@ def plot_values_bending_moment( def plot_values_axial_force( element: "Element", factor: float, n: int ) -> Tuple[np.ndarray, np.ndarray]: - """ - :param element: (object) of the Element class - :param factor: (float) scaling the plot - :param n: (integer) amount of x-values - :return: + """Determines the plotting values for axial force + + Args: + element (Element): Element to plot + factor (float): Factor by which to multiply the plotting values perpendicular to the elements axis. + n (int): Number of points to plot + + Returns: + Tuple[np.ndarray, np.ndarray]: x and y values """ # Determine forces for horizontal element.angle = 0 @@ -148,6 +157,15 @@ def plot_values_axial_force( def plot_values_shear_force( element: "Element", factor: float ) -> Tuple[np.ndarray, np.ndarray]: + """Determines the plotting values for shear force + + Args: + element (Element): Element to plot + factor (float): Factor by which to multiply the plotting values perpendicular to the elements axis. + + Returns: + Tuple[np.ndarray, np.ndarray]: x and y values + """ x1 = element.vertex_1.x y1 = -element.vertex_1.z x2 = element.vertex_2.x @@ -179,6 +197,14 @@ def plot_values_shear_force( def plot_values_element(element: "Element") -> Tuple[np.ndarray, np.ndarray]: + """Determines the plotting values for the element itself (e.g. for plot_structure()) + + Args: + element (Element): Element to plot + + Returns: + Tuple[np.ndarray, np.ndarray]: x and y values + """ x_val = np.array([element.vertex_1.x, element.vertex_2.x]) y_val = np.array([-element.vertex_1.z, -element.vertex_2.z]) return x_val, y_val diff --git a/anastruct/fem/plotter/mpl.py b/anastruct/fem/plotter/mpl.py index 2889e4e8..1dbb891a 100644 --- a/anastruct/fem/plotter/mpl.py +++ b/anastruct/fem/plotter/mpl.py @@ -29,6 +29,12 @@ 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 + """ self.plot_values = PlottingValues(system, mesh) self.mesh: int = self.plot_values.mesh self.system: "SystemElements" = system @@ -41,11 +47,24 @@ def __init__(self, system: "SystemElements", mesh: int): @property 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 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)] @@ -53,8 +72,11 @@ def __start_plot( return (self.fig.get_figwidth(), self.fig.get_figheight()) def __fixed_support_patch(self, max_val: float, axes_i: int = 0) -> None: - """ - :param max_val: max scale of the plot + """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: @@ -68,8 +90,11 @@ def __fixed_support_patch(self, max_val: float, axes_i: int = 0) -> None: self.axes[axes_i].add_patch(support_patch) def __hinged_support_patch(self, max_val: float, axes_i: int = 0) -> None: - """ - :param max_val: max scale of the plot + """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: @@ -83,8 +108,11 @@ def __hinged_support_patch(self, max_val: float, axes_i: int = 0) -> None: self.axes[axes_i].add_patch(support_patch) def __rotational_support_patch(self, max_val: float, axes_i: int = 0) -> None: - """ - :param max_val: max scale of the plot + """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: @@ -99,8 +127,11 @@ def __rotational_support_patch(self, max_val: float, axes_i: int = 0) -> None: self.axes[axes_i].add_patch(support_patch) def __roll_support_patch(self, max_val: float, axes_i: int = 0) -> None: - """ - :param max_val: max scale of the plot + """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 @@ -190,8 +221,11 @@ def __roll_support_patch(self, max_val: float, axes_i: int = 0) -> None: count += 1 def __rotating_spring_support_patch(self, max_val: float, axes_i: int = 0) -> None: - """ - :param max_val: max scale of the plot + """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 @@ -213,8 +247,11 @@ def __rotating_spring_support_patch(self, max_val: float, axes_i: int = 0) -> No self.axes[axes_i].add_patch(support_patch) def __spring_support_patch(self, max_val: float, axes_i: int = 0) -> None: - """ - :param max_val: max scale of the plot + """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 @@ -258,13 +295,17 @@ def __spring_support_patch(self, max_val: float, axes_i: int = 0) -> None: self.axes[axes_i].add_patch(support_patch) def __q_load_patch(self, max_val: float, verbosity: int, axes_i: int = 0) -> None: - """ - :param max_val: max scale of the plot + """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( @@ -280,6 +321,21 @@ def __plot_patch( 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 z 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 @@ -392,12 +448,16 @@ def __plot_patch( def __arrow_patch_values( Fx: float, Fz: float, node: "Node", h: float ) -> Tuple[float, float, float, float, float]: - """ - :param Fx: (float) - :param Fz: (float) - :param node: (Node object) - :param h: (float) Is a scale variable - :return: Variables for the matplotlib plotter + """Determines the values for the point load arrow patch. + + Args: + Fx (float): Point load magnitude in x direction + Fz (float): Point load magnitude in z 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 + Fz**2) ** 0.5 @@ -411,8 +471,12 @@ def __arrow_patch_values( def __point_load_patch( self, max_plot_range: float, verbosity: int = 0, axes_i: int = 0 ) -> None: - """ - :param max_plot_range: max scale of the plot + """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: @@ -437,6 +501,12 @@ def __point_load_patch( self.axes[axes_i].text(x, y, f"F={F}", color="k", fontsize=9, zorder=10) 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] @@ -477,12 +547,21 @@ def plot_structure( annotations: bool = True, axes_i: int = 0, ) -> Optional["Figure"]: - """ - :param show: (boolean) if True, plt.figure will plot. - :param supports: (boolean) if True, supports are plotted. - :param annotations: (boolean) if True, structure annotations are plotted. - It includes section name. - :return: + """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) @@ -585,6 +664,16 @@ def _add_node_values( 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 @@ -614,6 +703,16 @@ def _add_element_values( 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], @@ -634,6 +733,18 @@ def plot_result( color: int = 0, 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=f"C{color}", alpha=0.3 @@ -649,6 +760,7 @@ def plot_result( self._add_node_values(x_val, y_val, force_1, force_2, digits) def plot(self) -> None: + """Plots the figure.""" plt.show() # type: ignore def axial_force( @@ -662,6 +774,21 @@ def axial_force( 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 ) @@ -753,6 +880,21 @@ def bending_moment( 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 ) @@ -820,6 +962,22 @@ def shear_force( 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 @@ -866,6 +1024,20 @@ def reaction_force( 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, @@ -992,6 +1164,22 @@ def displacements( # pylint: disable=arguments-renamed 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 ) @@ -1046,15 +1234,17 @@ def results_plot( offset: Sequence[float], show: bool, ) -> Optional["Figure"]: - """ - Aggregate all the plots in one grid plot. - - :param figsize: (tpl) - :param verbosity: (int) - :param scale: (flt) - :param offset: (tpl) - :param show: (bool) - :return: Figure or None + """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 = [] diff --git a/anastruct/fem/plotter/values.py b/anastruct/fem/plotter/values.py index 0689d7bd..b94f8489 100644 --- a/anastruct/fem/plotter/values.py +++ b/anastruct/fem/plotter/values.py @@ -1,5 +1,5 @@ import math -from typing import TYPE_CHECKING, Optional, Tuple, Union +from typing import TYPE_CHECKING, Optional, Tuple import numpy as np @@ -18,6 +18,15 @@ def det_scaling_factor(max_unit: float, max_val_structure: float) -> float: + """Determines the scaling factor for the plotting values. + + Args: + max_unit (float): Maximum value of the unit to be plotted + max_val_structure (float): Maximum value of the structure coordinates + + Returns: + float: Scaling factor + """ if math.isclose(max_unit, 0): return 0.1 return 0.15 * max_val_structure / max_unit @@ -25,6 +34,12 @@ def det_scaling_factor(max_unit: float, max_val_structure: float) -> float: class PlottingValues: def __init__(self, system: "SystemElements", mesh: int): + """Class for determining the plotting values of the elements. + + Args: + system (SystemElements): System of elements + mesh (int): Number of points to plot + """ self.system: "SystemElements" = system self.mesh: int = max(3, mesh) # used for scaling the plotting values. @@ -32,9 +47,10 @@ def __init__(self, system: "SystemElements", mesh: int): @property def max_val_structure(self) -> float: - """ - Determine the maximum value of the structures dimensions. - :return: (flt) + """Maximum value of the structures dimensions. + + Returns: + float: Maximum value of the structures dimensions. """ if self._max_val_structure is None: self._max_val_structure = max( @@ -50,6 +66,15 @@ def max_val_structure(self) -> float: def displacements( self, factor: Optional[float], linear: bool ) -> Tuple[np.ndarray, np.ndarray]: + """Determines the plotting values for displacements + + Args: + factor (Optional[float]): Factor by which to multiply the plotting values perpendicular to the elements axis + linear (bool): If True, the bending in between the elements is determined. + + Returns: + Tuple[np.ndarray, np.ndarray]: x and y values + """ if factor is None: # needed to determine the scaling factor max_displacement = max( @@ -72,6 +97,14 @@ def displacements( return xy[0, :], xy[1, :] def bending_moment(self, factor: Optional[float]) -> Tuple[np.ndarray, np.ndarray]: + """Determines the plotting values for bending moment + + Args: + factor (Optional[float]): Factor by which to multiply the plotting values perpendicular to the elements axis + + Returns: + Tuple[np.ndarray, np.ndarray]: x and y values + """ if factor is None: # maximum moment determined by comparing the node's moments and the sagging moments. max_moment = max( @@ -96,9 +129,12 @@ def bending_moment(self, factor: Optional[float]) -> Tuple[np.ndarray, np.ndarra ) return xy[0, :], xy[1, :] - def axial_force( - self, factor: Optional[float] - ) -> Optional[Union[Tuple[np.ndarray, np.ndarray], "Figure"]]: + def axial_force(self, factor: Optional[float]) -> Tuple[np.ndarray, np.ndarray]: + """Determines the plotting values for axial force + + Returns: + Tuple[np.ndarray, np.ndarray]: x and y values + """ if factor is None: max_force = max( map( @@ -122,6 +158,14 @@ def axial_force( return xy[0, :], xy[1, :] def shear_force(self, factor: Optional[float]) -> Tuple[np.ndarray, np.ndarray]: + """Determines the plotting values for shear force + + Args: + factor (Optional[float]): Factor by which to multiply the plotting values perpendicular to the elements axis + + Returns: + Tuple[np.ndarray, np.ndarray]: x and y values + """ if factor is None: max_force = max( map( @@ -139,6 +183,11 @@ def shear_force(self, factor: Optional[float]) -> Tuple[np.ndarray, np.ndarray]: return xy[0, :], xy[1, :] def structure(self) -> Tuple[np.ndarray, np.ndarray]: + """Determines the plotting values for the structure itself + + Returns: + Tuple[np.ndarray, np.ndarray]: x and y values + """ xy = np.hstack( [plot_values_element(el) for el in self.system.element_map.values()] ) From a3c7487e4e50b0f9f1c86c4d3941c3e05b017834 Mon Sep 17 00:00:00 2001 From: Brooks Smith Date: Tue, 24 Oct 2023 22:46:30 +1100 Subject: [PATCH 10/20] Fix advanced type error in Vertex == Raise the error; don't return it --- anastruct/vertex.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/anastruct/vertex.py b/anastruct/vertex.py index cbad074d..c0369b20 100644 --- a/anastruct/vertex.py +++ b/anastruct/vertex.py @@ -194,12 +194,15 @@ def __truediv__( coordinates = self.coordinates / other return Vertex(coordinates) - def __eq__(self, other: object) -> bool | NotImplementedError: + def __eq__(self, other: object) -> bool: """Check if two Vertex objects are equal Args: other (object): Object to compare + Raises: + NotImplementedError: If the object is not a Vertex object or a tuple or list of length 2 + Returns: bool: True if the two Vertex objects are equal """ @@ -207,7 +210,7 @@ def __eq__(self, other: object) -> bool | NotImplementedError: return self.x == other.x and self.y == other.y if isinstance(other, (tuple, list)) and len(other) == 2: return self.x == other[0] and self.y == other[1] - return NotImplemented + raise NotImplementedError def __str__(self) -> str: """String representation of the Vertex object From 9c87e116b24ef1f474bee4b95f31d656b1ef6f27 Mon Sep 17 00:00:00 2001 From: Brooks Smith Date: Tue, 24 Oct 2023 22:48:56 +1100 Subject: [PATCH 11/20] TODO -> TO DO --- anastruct/fem/system.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/anastruct/fem/system.py b/anastruct/fem/system.py index 8b79df08..677893e6 100644 --- a/anastruct/fem/system.py +++ b/anastruct/fem/system.py @@ -218,7 +218,7 @@ def add_element_grid( FEMException: x and y should have the same length. FEMException: The mp parameter should be a dictionary. """ - # TODO: Why doesn't this function use Vertex objects? It's the only function which has _separated_ x and y lists + # TO DO: Why doesn't this function use Vertex objects? It's the only function which has _separate_ x and y lists if len(x) != len(y): raise FEMException( "Wrong parameters", "x and y should have the same length." @@ -1389,7 +1389,7 @@ def get_node_results_system( If node_id > 0, returns a dict with the results: {"id": id, "Fx": Fx, "Fy": Fy, "Ty": Ty, "ux": ux, "uy": uy, "phi_y": phi_y} """ - # TODO: This should return a List of Dicts, not a list of Tuples... + # TO DO: This should return a List of Dicts, not a list of Tuples... result_list = [] if node_id != 0: node_id = _negative_index_to_id(node_id, self.node_map) @@ -1423,7 +1423,7 @@ def get_node_displacements( tuples with the results: [(id, ux, uy, phi_y), (id, ux, uy, phi_y), ... (id, ux, uy, phi_y) ] If node_id > 0, returns a dict with the results: {"id": id, "ux": ux, "uy": uy, "phi_y": phi_y} """ - # TODO: This should return a List of Dicts, not a list of Tuples... + # TO DO: This should return a List of Dicts, not a list of Tuples... result_list = [] if node_id != 0: node_id = _negative_index_to_id(node_id, self.node_map) @@ -1566,7 +1566,7 @@ def get_element_result_range( Returns: List[float]: List with the first mesh node results of each element for a certain unit. """ - # TODO: This function does not make sense... Unclear why I should care about only the first node of each element + # TO DO: This function does not make sense... Unclear why I should care about only the 1st node of each element if unit == "shear": return [ el.shear_force[0] From d3a46a50d5c3ec7e356dcfccd0696bfaff3aae94 Mon Sep 17 00:00:00 2001 From: Brooks Smith Date: Tue, 24 Oct 2023 22:59:54 +1100 Subject: [PATCH 12/20] Fix another typing issue in Vertex --- anastruct/vertex.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/anastruct/vertex.py b/anastruct/vertex.py index c0369b20..f9916bb8 100644 --- a/anastruct/vertex.py +++ b/anastruct/vertex.py @@ -208,9 +208,14 @@ def __eq__(self, other: object) -> bool: """ if isinstance(other, Vertex): return self.x == other.x and self.y == other.y - if isinstance(other, (tuple, list)) and len(other) == 2: + if ( + isinstance(other, (tuple, list)) + and len(other) == 2 + and isinstance(other[0], (int, float)) + and isinstance(other[1], (int, float)) + ): return self.x == other[0] and self.y == other[1] - raise NotImplementedError + return NotImplemented def __str__(self) -> str: """String representation of the Vertex object From e57d7a2713747eda7181380f573791d1632d9ff3 Mon Sep 17 00:00:00 2001 From: Brooks Smith Date: Tue, 24 Oct 2023 23:15:33 +1100 Subject: [PATCH 13/20] Bugfix: assume no hinges on initial element creation --- anastruct/fem/elements.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/anastruct/fem/elements.py b/anastruct/fem/elements.py index 20accf23..2cc91502 100644 --- a/anastruct/fem/elements.py +++ b/anastruct/fem/elements.py @@ -198,6 +198,10 @@ def compile_kinematic_matrix(self) -> None: def compile_constitutive_matrix(self) -> None: """Compile the constitutive matrix of the element""" + if self.node_map is None: # if element is just being created + self.constitutive_matrix = constitutive_matrix( + self.EA, self.EI, self.l, self.springs, False, False + ) self.constitutive_matrix = constitutive_matrix( self.EA, self.EI, self.l, self.springs, self.node_1.hinge, self.node_2.hinge ) From 867de40d0a6888676b54f97470db96c9a8a1856d Mon Sep 17 00:00:00 2001 From: Brooks Smith Date: Tue, 24 Oct 2023 23:16:24 +1100 Subject: [PATCH 14/20] And indent... --- anastruct/fem/elements.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/anastruct/fem/elements.py b/anastruct/fem/elements.py index 2cc91502..0e62832e 100644 --- a/anastruct/fem/elements.py +++ b/anastruct/fem/elements.py @@ -202,9 +202,15 @@ def compile_constitutive_matrix(self) -> None: self.constitutive_matrix = constitutive_matrix( self.EA, self.EI, self.l, self.springs, False, False ) - self.constitutive_matrix = constitutive_matrix( - self.EA, self.EI, self.l, self.springs, self.node_1.hinge, self.node_2.hinge - ) + else: + self.constitutive_matrix = constitutive_matrix( + self.EA, + self.EI, + self.l, + self.springs, + self.node_1.hinge, + self.node_2.hinge, + ) def update_stiffness(self, factor: float, node: Literal[1, 2]) -> None: """Update the stiffness matrix of the element From 3c231c78e69f3dc4090957e032834c9730719f4a Mon Sep 17 00:00:00 2001 From: Brooks Smith Date: Tue, 24 Oct 2023 23:25:42 +1100 Subject: [PATCH 15/20] Swap syntax --- anastruct/fem/elements.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/anastruct/fem/elements.py b/anastruct/fem/elements.py index 0e62832e..bc321ae7 100644 --- a/anastruct/fem/elements.py +++ b/anastruct/fem/elements.py @@ -198,11 +198,7 @@ def compile_kinematic_matrix(self) -> None: def compile_constitutive_matrix(self) -> None: """Compile the constitutive matrix of the element""" - if self.node_map is None: # if element is just being created - self.constitutive_matrix = constitutive_matrix( - self.EA, self.EI, self.l, self.springs, False, False - ) - else: + if self.node_map: # if element is just being created self.constitutive_matrix = constitutive_matrix( self.EA, self.EI, @@ -211,6 +207,10 @@ def compile_constitutive_matrix(self) -> None: self.node_1.hinge, self.node_2.hinge, ) + else: + self.constitutive_matrix = constitutive_matrix( + self.EA, self.EI, self.l, self.springs, False, False + ) def update_stiffness(self, factor: float, node: Literal[1, 2]) -> None: """Update the stiffness matrix of the element From e84edfd657ad2251d31f951aa6b79de4055e68eb Mon Sep 17 00:00:00 2001 From: Brooks Smith Date: Tue, 24 Oct 2023 23:29:15 +1100 Subject: [PATCH 16/20] Create an 'initial state' variable for constitutive matrix compilation --- anastruct/fem/elements.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/anastruct/fem/elements.py b/anastruct/fem/elements.py index bc321ae7..5b220cda 100644 --- a/anastruct/fem/elements.py +++ b/anastruct/fem/elements.py @@ -95,7 +95,7 @@ def __init__( self.max_deflection = None self.max_extension = None self.nodes_plastic: List[bool] = [False, False] - self.compile_constitutive_matrix() + self.compile_constitutive_matrix(initial=True) self.compile_stiffness_matrix() self.section_name = section_name # needed for element annotation @@ -196,9 +196,13 @@ def compile_kinematic_matrix(self) -> None: """Compile the kinematic matrix of the element""" self.kinematic_matrix = kinematic_matrix(self.a1, self.a2, self.l) - def compile_constitutive_matrix(self) -> None: + def compile_constitutive_matrix(self, initial=False) -> None: """Compile the constitutive matrix of the element""" - if self.node_map: # if element is just being created + if initial: # if element is just being created + self.constitutive_matrix = constitutive_matrix( + self.EA, self.EI, self.l, self.springs, False, False + ) + else: self.constitutive_matrix = constitutive_matrix( self.EA, self.EI, @@ -207,10 +211,6 @@ def compile_constitutive_matrix(self) -> None: self.node_1.hinge, self.node_2.hinge, ) - else: - self.constitutive_matrix = constitutive_matrix( - self.EA, self.EI, self.l, self.springs, False, False - ) def update_stiffness(self, factor: float, node: Literal[1, 2]) -> None: """Update the stiffness matrix of the element From b7e698b24bcc4ddb4eb0562be89aa725c4a95a5f Mon Sep 17 00:00:00 2001 From: Brooks Smith Date: Tue, 24 Oct 2023 23:34:45 +1100 Subject: [PATCH 17/20] np.ceil() should really default to returning an int... --- anastruct/fem/system.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/anastruct/fem/system.py b/anastruct/fem/system.py index 677893e6..83cafbaa 100644 --- a/anastruct/fem/system.py +++ b/anastruct/fem/system.py @@ -527,7 +527,7 @@ def add_multiple_elements( direction = (point_2 - point_1).unit() if dl is None and n is not None: - var_n = np.ceil(n) + var_n = int(np.ceil(n)) lengths = np.linspace(start=0, stop=length, num=var_n + 1) elif dl is not None and n is None: var_n = int(np.ceil(length / dl) - 1) From 6bf02439c6c759ce40367f298e51c22f1fdba117 Mon Sep 17 00:00:00 2001 From: Brooks Smith Date: Tue, 24 Oct 2023 23:46:39 +1100 Subject: [PATCH 18/20] Fix list vs scalar type error --- anastruct/fem/plotter/mpl.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/anastruct/fem/plotter/mpl.py b/anastruct/fem/plotter/mpl.py index 1dbb891a..cc1c0f9f 100644 --- a/anastruct/fem/plotter/mpl.py +++ b/anastruct/fem/plotter/mpl.py @@ -985,7 +985,9 @@ def shear_force( if factor is None: max_force = max( map( - lambda el: np.max(np.abs(el.shear_force or 0.0)), + lambda el: np.max(np.abs(el.shear_force)) + if el.shear_force + else 0.0, self.system.element_map.values(), ) ) From 279dfc0ea44885db0f06e7d767a2bef9a973790b Mon Sep 17 00:00:00 2001 From: Brooks Smith Date: Tue, 24 Oct 2023 23:48:34 +1100 Subject: [PATCH 19/20] Fix --- anastruct/fem/plotter/mpl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/anastruct/fem/plotter/mpl.py b/anastruct/fem/plotter/mpl.py index cc1c0f9f..4aea0862 100644 --- a/anastruct/fem/plotter/mpl.py +++ b/anastruct/fem/plotter/mpl.py @@ -986,7 +986,7 @@ def shear_force( max_force = max( map( lambda el: np.max(np.abs(el.shear_force)) - if el.shear_force + if el.shear_force is not None else 0.0, self.system.element_map.values(), ) From 0e900d85db3cf55802844827bff460dc2285f1d8 Mon Sep 17 00:00:00 2001 From: Brooks Smith Date: Tue, 24 Oct 2023 23:52:49 +1100 Subject: [PATCH 20/20] Add type annotation, remove subtype --- anastruct/basic.py | 4 ++-- anastruct/fem/elements.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/anastruct/basic.py b/anastruct/basic.py index 4d7dfba3..1b70dbdc 100644 --- a/anastruct/basic.py +++ b/anastruct/basic.py @@ -11,11 +11,11 @@ from anastruct.cython.basic import angle_x_axis, converge -def find_nearest(array: np.ndarray[float, Any], value: float) -> Tuple[float, int]: +def find_nearest(array: np.ndarray, value: float) -> Tuple[float, int]: """Find the nearest value in an array Args: - array (np.ndarray[float, Any]): array to search within + array (np.ndarray): array to search within value (float): value to search for Returns: diff --git a/anastruct/fem/elements.py b/anastruct/fem/elements.py index 5b220cda..85df4041 100644 --- a/anastruct/fem/elements.py +++ b/anastruct/fem/elements.py @@ -196,7 +196,7 @@ def compile_kinematic_matrix(self) -> None: """Compile the kinematic matrix of the element""" self.kinematic_matrix = kinematic_matrix(self.a1, self.a2, self.l) - def compile_constitutive_matrix(self, initial=False) -> None: + def compile_constitutive_matrix(self, initial: bool = False) -> None: """Compile the constitutive matrix of the element""" if initial: # if element is just being created self.constitutive_matrix = constitutive_matrix(