Source code for planestress.pre.boundary_condition

"""Classes describing a planestress boundary conditions.

Boundary condition application priorities:
0 - Loads
1 - Springs
2 - Supports
"""

from __future__ import annotations

from typing import TYPE_CHECKING, cast

import numpy as np
import numpy.typing as npt

from planestress.analysis.utils import dof_map


if TYPE_CHECKING:
    from scipy.sparse import lil_array

    from planestress.pre.mesh import TaggedEntity, TaggedLine, TaggedNode


[docs] class BoundaryCondition: """Abstract base class for a boundary condition. Attributes: mesh_tag: Tagged entity object. """
[docs] def __init__( self, direction: str, value: float, priority: int, ) -> None: """Inits the BoundaryCondition class. Args: direction: Direction of the boundary condition, ``"x"``, ``"y"`` or ``"xy"``. value: Value of the boundary condition. priority: Integer denoting the order in which the boundary condition gets applied. """ self.direction = direction # TODO - verify input self.value = value self.priority = priority self.mesh_tag: TaggedEntity | None = None
[docs] def apply_bc( self, k: lil_array, f: npt.NDArray[np.float64], ) -> tuple[lil_array, npt.NDArray[np.float64]]: """Applies the boundary condition. Args: k: Stiffness matrix. f: Load vector. Raises: NotImplementedError: If this method has not been implemented. """ raise NotImplementedError
[docs] def get_dofs_given_direction( self, dofs: list[int], ) -> list[int]: """Gets the degrees of freedom based on the BC direction. Args: dofs: Degrees of freedom. Returns: Degrees of freeom in BC direction. """ # get relevant dofs if self.direction == "x": dofs = dofs[0::2] elif self.direction == "y": dofs = dofs[1::2] else: dofs = dofs return dofs
[docs] class NodeBoundaryCondition(BoundaryCondition): """Abstract base class for a boundary condition at a node. Attributes: mesh_tag: Tagged node object. """
[docs] def __init__( self, point: tuple[float, float], direction: str, value: float, priority: int, ) -> None: """Inits the NodeBoundaryCondition class. Args: point: Point tuple (``x``, ``y``) describing the node location. direction: Direction of the boundary condition, ``"x"``, ``"y"`` or ``"xy"``. value: Value of the boundary condition. priority: Integer denoting the order in which the boundary condition gets applied. """ super().__init__(direction=direction, value=value, priority=priority) self.point = point self.mesh_tag: TaggedNode | None
def __repr__(self) -> str: """Override __repr__ method. Returns: String representation of the object. """ return ( f"BC Type: {self.__class__.__name__}, dir: {self.direction}, val: " f"{self.value}, mesh tag: {self.mesh_tag}" )
[docs] def get_node_dofs(self) -> list[int]: """Get the degrees of freedom of the node. Raises: RuntimeError: If a mesh tag has not been assigned. Returns: List (length 2) of degrees of freedom. """ if self.mesh_tag is None: raise RuntimeError("Mesh tag is not assigned.") return dof_map(node_idxs=(self.mesh_tag.node_idx,))
[docs] class NodeSupport(NodeBoundaryCondition): """Class for adding a support to a node. Attributes: mesh_tag: Tagged node object. """
[docs] def __init__( self, point: tuple[float, float], direction: str, value: float = 0.0, ) -> None: """Inits the NodeSupport class. Args: point: Point location (``x``, ``y``) of the node support. direction: Direction of the node support, either ``"x"`` or ``"y"`` (rollers), or ``"xy"`` (pin). value: Imposed displacement to apply to the node support. Defaults to ``0.0``, i.e. a node support with fixed zero displacement. Example: TODO. """ super().__init__(point=point, direction=direction, value=value, priority=2)
[docs] def apply_bc( self, k: lil_array, f: npt.NDArray[np.float64], ) -> tuple[lil_array, npt.NDArray[np.float64]]: """Applies the boundary condition. Args: k: Stiffness matrix. f: Load vector. Returns: Modified stiffness matrix and load vector (``k``, ``f``). """ # get nodal dofs dofs = self.get_node_dofs() # get relevant dofs dofs = self.get_dofs_given_direction(dofs=dofs) for dof in dofs: # apply bc - TODO - confirm this theory! k[dof, :] = 0 k[dof, dof] = 1.0 f[dof] = self.value return k, f
[docs] class NodeSpring(NodeBoundaryCondition): """Class for adding a spring to a node. Attributes: mesh_tag: Tagged node object. """
[docs] def __init__( self, point: tuple[float, float], direction: str, value: float, ) -> None: """Inits the NodeSpring class. Args: point: Point location (``x``, ``y``) of the node spring. direction: Direction of the node spring, ``"x"``, ``"y"`` or ``"xy"``. value: Spring stiffness. Example: TODO. """ super().__init__(point=point, direction=direction, value=value, priority=1)
[docs] def apply_bc( self, k: lil_array, f: npt.NDArray[np.float64], ) -> tuple[lil_array, npt.NDArray[np.float64]]: """Applies the boundary condition. Args: k: Stiffness matrix. f: Load vector. Returns: Modified stiffness matrix and load vector (``k``, ``f``). """ # get nodal dofs dofs = self.get_node_dofs() # get relevant dofs dofs = self.get_dofs_given_direction(dofs=dofs) for dof in dofs: # apply bc - TODO - confirm this theory! k[dof, dof] = cast(float, k[dof, dof]) + self.value return k, f
[docs] class NodeLoad(NodeBoundaryCondition): """Class for adding a load to a node. Attributes: mesh_tag: Tagged node object. """
[docs] def __init__( self, point: tuple[float, float], direction: str, value: float, ) -> None: """Inits the NodeLoad class. Args: point: Point location (``x``, ``y``) of the node load. direction: Direction of the node load, ``"x"``, ``"y"`` or ``"xy"`` (two point loads in both ``x`` and ``y`` directions). value: Node load. Example: TODO. """ super().__init__(point=point, direction=direction, value=value, priority=0)
[docs] def apply_bc( self, k: lil_array, f: npt.NDArray[np.float64], ) -> tuple[lil_array, npt.NDArray[np.float64]]: """Applies the boundary condition. Args: k: Stiffness matrix. f: Load vector. Returns: Modified stiffness matrix and load vector (``k``, ``f``). """ # get nodal dofs dofs = self.get_node_dofs() # get relevant dofs dofs = self.get_dofs_given_direction(dofs=dofs) for dof in dofs: # apply bc f[dof] += self.value return k, f
[docs] class LineBoundaryCondition(BoundaryCondition): """Abstract base class for a boundary condition along a line. Attributes: mesh_tag: Tagged line object. """
[docs] def __init__( self, point1: tuple[float, float], point2: tuple[float, float], direction: str, value: float, priority: int, ) -> None: """Inits the LineBoundaryCondition class. Args: point1: Point location (``x``, ``y``) of the start of the line. point2: Point location (``x``, ``y``) of the end of the line. direction: Direction of the boundary condition, ``"x"``, ``"y"`` or ``"xy"``. value: Value of the boundary condition. priority: Integer denoting the order in which the boundary condition gets applied. """ super().__init__(direction=direction, value=value, priority=priority) self.point1 = point1 self.point2 = point2 self.mesh_tag: TaggedLine | None = None
def __repr__(self) -> str: """Override __repr__ method. Returns: String representation of the object. """ return ( f"BC Type: {self.__class__.__name__}, dir: {self.direction}, val: " f"{self.value}, mesh tag: {self.mesh_tag}" )
[docs] def get_unique_nodes(self) -> list[int]: """Returns a list of unique node indexes along the line BC. Raises: RuntimeError: If a mesh tag has not been assigned. Returns: List of unique node indexes along the line. """ if self.mesh_tag is None: raise RuntimeError("Mesh tag is not assigned.") # get list of node indexes along line BC node_idxs = [] # loop through all line elements along the line BC for line_el in self.mesh_tag.elements: # loop through all nodes that make up the line element for node_idx in line_el.node_idxs: # if we haven't encountered this node, add it to the list if node_idx not in node_idxs: node_idxs.append(node_idx) return node_idxs
[docs] class LineSupport(LineBoundaryCondition): """Class for adding supports along a line. Attributes: mesh_tag: Tagged line object. """
[docs] def __init__( self, point1: tuple[float, float], point2: tuple[float, float], direction: str, value: float = 0.0, ) -> None: """Inits the LineSupport class. Args: point1: Point location (``x``, ``y``) of the start of the line support. point2: Point location (``x``, ``y``) of the end of the line support. direction: Direction of the line support, either ``"x"`` or ``"y"`` (rollers), or ``"xy"`` (pin). value: Imposed displacement to apply to the line support. Defaults to ``0.0``, i.e. a line support with fixed zero displacement. Example: TODO. """ super().__init__( point1=point1, point2=point2, direction=direction, value=value, priority=2 )
[docs] def apply_bc( self, k: lil_array, f: npt.NDArray[np.float64], ) -> tuple[lil_array, npt.NDArray[np.float64]]: """Applies the boundary condition. Args: k: Stiffness matrix. f: Load vector. Returns: Modified stiffness matrix and load vector (``k``, ``f``). """ # get degrees of freedom for node indexes dofs = dof_map(node_idxs=tuple(self.get_unique_nodes())) # get relevant dofs dofs = self.get_dofs_given_direction(dofs=dofs) # apply bc - TODO - confirm this theory! for dof in dofs: k[dof, :] = 0 k[dof, dof] = 1 f[dof] = self.value return k, f
[docs] class LineSpring(LineBoundaryCondition): """Class for adding springs along a line. Attributes: mesh_tag: Tagged line object. """
[docs] def __init__( self, point1: tuple[float, float], point2: tuple[float, float], direction: str, value: float, ) -> None: """Inits the LineSpring class. Args: point1: Point location (``x``, ``y``) of the start of the line spring. point2: Point location (``x``, ``y``) of the end of the line spring. direction: Direction of the line spring, either ``"x"``, ``"y"`` or ``"xy"``. value: Spring stiffness per unit length. Example: TODO. """ super().__init__( point1=point1, point2=point2, direction=direction, value=value, priority=1 )
[docs] def apply_bc( self, k: lil_array, f: npt.NDArray[np.float64], ) -> tuple[lil_array, npt.NDArray[np.float64]]: """Applies the boundary condition. Args: k: Stiffness matrix. f: Load vector. Returns: Modified stiffness matrix and load vector (``k``, ``f``). """ # get degrees of freedom for node indexes dofs = dof_map(node_idxs=tuple(self.get_unique_nodes())) # get relevant dofs dofs = self.get_dofs_given_direction(dofs=dofs) # apply bc - TODO - confirm this theory! for dof in dofs: k[dof, dof] = cast(float, k[dof, dof]) + self.value return k, f
[docs] class LineLoad(LineBoundaryCondition): """Class for adding a load to a line. Attributes: mesh_tag: Tagged line object. """
[docs] def __init__( self, point1: tuple[float, float], point2: tuple[float, float], direction: str, value: float, ) -> None: """Inits the LineLoad class. Args: point1: Point location (``x``, ``y``) of the start of the line load. point2: Point location (``x``, ``y``) of the end of the line load. direction: Direction of the line load, ``"x"``, ``"y"`` or ``"xy"`` (two line loads in both ``x`` and ``y`` directions). value: Line load per unit length. Example: TODO. """ super().__init__( point1=point1, point2=point2, direction=direction, value=value, priority=0 )
[docs] def apply_bc( self, k: lil_array, f: npt.NDArray[np.float64], ) -> tuple[lil_array, npt.NDArray[np.float64]]: """Applies the boundary condition. Args: k: Stiffness matrix. f: Load vector. Raises: RuntimeError: If a mesh tag has not been assigned. Returns: Modified stiffness matrix and load vector (``k``, ``f``). """ if self.mesh_tag is None: raise RuntimeError("Mesh tag is not assigned.") # loop through all line elements for element in self.mesh_tag.elements: # get element load vector f_el = element.element_load_vector( direction=self.direction, value=self.value ) # get element degrees of freedom el_dofs = dof_map(node_idxs=tuple(element.node_idxs)) # add element load vector to global load vector f[el_dofs] += f_el return k, f