Source code for opstool.pre.load._load_transform

from collections import defaultdict
from typing import Union

import numpy as np

from ...utils import get_opensees_module

ops = get_opensees_module()


def _construct_transform_matrix_beam(ele_tags):
    """
    Constructs the transformation matrix from the global coordinate system to the local coordinate system.

    Returns
    -------
    T : numpy.ndarray
        A 3x3 transformation matrix mapping global coordinates to local coordinates.
    ndim: int
        The dimensions of the model.
    """
    # Calculate the local coordinate axes
    ele_tags = np.atleast_1d(ele_tags)
    ele_tags = [int(tag) for tag in ele_tags]
    T = []
    ndim = 2
    for etag in ele_tags:
        ele_nodes = ops.eleNodes(etag)
        coords = ops.nodeCoord(ele_nodes[0])
        ndim_ = len(coords)
        if ndim_ > ndim:
            ndim = ndim_
        xaxis = ops.eleResponse(etag, "xaxis")
        yaxis = ops.eleResponse(etag, "yaxis")
        zaxis = ops.eleResponse(etag, "zaxis")
        T.append([xaxis, yaxis, zaxis])
    return np.array(T), ndim, ele_tags


[docs] def transform_beam_uniform_load( ele_tags: Union[int, list[int], tuple, np.ndarray[int]], wx: Union[float, list[float], np.ndarray[float]] = 0.0, wy: Union[float, list[float], np.ndarray[float]] = 0.0, wz: Union[float, list[float], np.ndarray[float]] = 0.0, ) -> None: """ Transforms a uniformly distributed beam load from the global coordinate system to the local coordinate system. .. Note:: This function will automatically call the `EleLoad Command <https://opensees.berkeley.edu/wiki/index.php/EleLoad_Command>`_ to generate element loads. However, you need to create ``timeSeries`` and load ``pattern`` objects externally in advance. The load generated by this function will belong to the load pattern closest to it. Parameters ----------- ele_tags : int, list, tuple, np.ndarray Beam element tags. wx : float, list, np.ndarray, default=0.0 Uniformly distributed load in the `global X` direction. If a list or numpy array is provided, the length should be the same as the number of elements. wy : float, list, np.ndarray, default=0.0 Uniformly distributed load in the `global Y` direction. If a list or numpy array is provided, the length should be the same as the number of elements. wz : float, list, np.ndarray, default=0.0 Uniformly distributed load in the `global Z` direction. If a list or numpy array is provided, the length should be the same as the number of elements. """ T, ndim, ele_tags = _construct_transform_matrix_beam(ele_tags) q_globals = np.atleast_2d(np.array([wx, wy, wz])) if q_globals.shape[0] == 1 and T.shape[0] > 1: q_globals = np.repeat(q_globals, T.shape[0], axis=0) q_locals = np.einsum("nij,nj->ni", T, q_globals) if ndim == 3: for qlocal, etag in zip(q_locals, ele_tags): qlocal = [float(q) for q in qlocal] ops.eleLoad("-ele", etag, "-type", "-beamUniform", qlocal[1], qlocal[2], qlocal[0]) else: for qlocal, etag in zip(q_locals, ele_tags): qlocal = [float(q) for q in qlocal] ops.eleLoad("-ele", etag, "-type", "-beamUniform", qlocal[1], qlocal[0])
[docs] def transform_beam_point_load( ele_tags: Union[int, list[int], tuple, np.ndarray[int]], px: Union[float, list[float], np.ndarray[float]] = 0.0, py: Union[float, list[float], np.ndarray[float]] = 0.0, pz: Union[float, list[float], np.ndarray[float]] = 0.0, xl: Union[float, list[float], np.ndarray[float]] = 0.5, ) -> None: """ Transforms point loads for beam elements from global to local coordinates. Parameters ---------- ele_tags : int, list, tuple, np.ndarray Beam element tags. px : float, list, np.ndarray, default=0.0 Point load in the `global X` direction. If a list or numpy array is provided, the length should be the same as the number of elements. py : float, list, np.ndarray, default=0.0 Point load in the `global Y` direction. If a list or numpy array is provided, the length should be the same as the number of elements. pz : float, list, np.ndarray, default=0.0 Point load in the `global Z` direction. If a list or numpy array is provided, the length should be the same as the number of elements. xl : float, list, np.ndarray, default=0.5 The position of the point load along the beam element in a local coord system. If a list or numpy array is provided, the length should be the same as the number of elements. """ # Compute global positions of point loads T, ndim, ele_tags = _construct_transform_matrix_beam(ele_tags) p_globals = np.atleast_2d(np.array([px, py, pz])) if p_globals.shape[0] == 1 and T.shape[0] > 1: p_globals = np.repeat(p_globals, T.shape[0], axis=0) xls = np.atleast_2d(xl) if xls.shape[0] == 1 and T.shape[0] > 1: xls = np.repeat(xls, T.shape[0], axis=0) # Transform point loads and positions to local coordinates p_locals = np.einsum("nij,nj->ni", T, p_globals) if ndim == 3: for plocal, etag, xl in zip(p_locals, ele_tags, xls): plocal = [float(p) for p in plocal] ops.eleLoad("-ele", etag, "-type", "-beamPoint", plocal[1], plocal[2], xl[0], plocal[0]) else: for plocal, etag, xl in zip(p_locals, ele_tags, xls): plocal = [float(p) for p in plocal] ops.eleLoad("-ele", etag, "-type", "-beamPoint", plocal[1], xl[0], plocal[0])
[docs] def transform_surface_uniform_load( ele_tags: Union[int, list[int], tuple, np.ndarray[int]], p: Union[float, list[float], np.ndarray[float]] = 0.0, ) -> None: """ Converts uniform surface loads into equivalent nodal forces in the global coordinate system. According to the static equivalence principle, the distributed load is equivalent to the node load. .. Note:: This function will automatically call the `NodalLoad Command <https://opensees.berkeley.edu/wiki/index.php?title=NodalLoad_Command>`_ to generate nodal loads. However, you need to create ``timeSeries`` and load ``pattern`` objects externally in advance. The load generated by this function will belong to the load pattern closest to it. Parameters ---------- ele_tags : int, list, tuple, np.ndarray Surface element tags. p : float, list, np.ndarray, default=0.0 Uniform surface load magnitude (per unit area) along the surface normal direction. The positive direction of the normal is obtained by the cross-product of the I-J and J-K edges. If a list or numpy array is provided, the length should be the same as the number of elements. """ ele_tags = np.atleast_1d(ele_tags) ele_tags = [int(tag) for tag in ele_tags] uniform_loads = [p] * len(ele_tags) if isinstance(p, (int, float)) else p nodal_forces = defaultdict(lambda: np.zeros(3)) nodal_dofs = {} for etag, load in zip(ele_tags, uniform_loads): node_ids = ops.eleNodes(etag) vertices = np.array([ops.nodeCoord(node_id) for node_id in node_ids]) # Compute area and normal based on an element type if len(node_ids) == 3: # Triangle area, normal = _compute_tri_area_and_normal(vertices) elif len(node_ids) == 4: # Quadrilateral area, normal = _compute_quad_area_and_normal(vertices) else: raise ValueError(f"Unsupported element type with {len(node_ids)} nodes.") # noqa: TRY003 # Compute total force on the element element_force = load * area * normal # Total force vector in global coordinates # Distribute force to each node equally force_per_node = element_force / len(node_ids) # Accumulate forces to the global nodal forces dictionary for node_id in node_ids: nodal_forces[node_id] += force_per_node nodal_dofs[node_id] = ops.getNDF(node_id)[0] for key, value in nodal_forces.items(): ndf = nodal_dofs[key] if ndf == 3: ops.load(key, *value) elif ndf == 6: ops.load(key, *value, 0, 0, 0) elif ndf == 2: ops.load(key, *value[:2])
def _compute_tri_area_and_normal(vertices): """ Compute the area and normal vector of a triangular element. Parameters ---------- vertices : numpy.ndarray Coordinates of the triangle's vertices, shape (3, 3). Returns ------- area : float Area of the triangle. normal : numpy.ndarray Unit normal vector of the triangle, shape (3,). """ # Edges: IJ and JK edge_ij = vertices[1] - vertices[0] edge_jk = vertices[2] - vertices[1] # Compute cross product cross_product = np.cross(edge_ij, edge_jk) norm = np.linalg.norm(cross_product) area = 0.5 * norm # Normalize the normal vector normal = cross_product / norm return area, normal def _compute_quad_area_and_normal(vertices): """ Compute the area and normal vector of a quadrilateral element. Parameters ---------- vertices : numpy.ndarray Coordinates of the quadrilateral's vertices, shape (4, 3). Returns ------- area : float Area of the quadrilateral. normal : numpy.ndarray Unit normal vector of the quadrilateral, shape (3,). """ # Divide quadrilateral into two triangles triangle1 = vertices[:3] triangle2 = np.array([vertices[0], vertices[2], vertices[3]]) # Compute areas and normals area1, normal1 = _compute_tri_area_and_normal(triangle1) area2, normal2 = _compute_tri_area_and_normal(triangle2) # Average the normals and normalize normal = (normal1 + normal2) / 2.0 # normal = normal / np.linalg.norm(normal) return area1 + area2, normal