Source code for sectionproperties.post.fibre

"""Provides functionalities to export a section to a fibre section.

It can be used in `suanPan <https://github.com/TLCFEM/suanPan>`_ to perform further FEA.
"""

from __future__ import annotations

from abc import abstractmethod

import numpy as np
import numpy.typing as npt

import sectionproperties.analysis.solver as solver
from sectionproperties.analysis.fea import Tri6, shape_function
from sectionproperties.analysis.section import Section
from sectionproperties.pre.geometry import CompoundGeometry, Geometry


[docs] class Cell: """Holds the information of a fibre cell. Attributes: tag: The tag of the cell area: The area of the cell material: The material name of the cell y: The y-coordinate of the cell z: The z-coordinate of the cell omega: The warping function of the cell py: The derivative of the warping function with respect to y pz: The derivative of the warping function with respect to z """ tag: int area: float material: str y: float z: float omega: float py: float pz: float
[docs] def __init__( self, ele: Tri6, omega: npt.NDArray[np.float64] | None = None, ) -> None: """Converts a Tri6 element to a fibre cell. If `omega` is None, no warping is considered. Args: ele: The Tri6 element omega: The warping function """ n, dn, self.area, _, _ = shape_function( ele.coords, (0.0, 1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0) ) self.material = "-".join(ele.material.name.split()) self.tag = ele.el_id + 1 # el_id is zero-based self.y, self.z = ele.coords[:, :3].mean(axis=1) if omega is not None: ele_omega = omega[ele.node_ids] self.omega = ele_omega.dot(n) self.py, self.pz = dn @ ele_omega else: self.omega = 0.0 self.py = 0.0 self.pz = 0.0
[docs] @abstractmethod def export(self) -> str: """Export the cell to a string. Needs to be implemented by subclasses. Returns: str: The exported cell """ raise NotImplementedError
[docs] class Cell2D(Cell): """A 2D cell section."""
[docs] def export(self) -> str: """Export the cell to a 2D cell section. Returns: str: The exported cell """ return f"section Cell2D {self.tag} {self.area:.8e} {self.material} {self.y:.8e}"
[docs] class Cell3D(Cell): """A 3D cell section."""
[docs] def export(self) -> str: """Export the cell to a 3D cell section. Returns: str: The exported cell """ return ( f"section Cell3D {self.tag} {self.area:.8e} {self.material}" f" {self.y:.8e} {self.z:.8e}" )
[docs] class Cell3DOS(Cell): """A 3DOS cell section."""
[docs] def export(self) -> str: """Export the cell to a 3DOS cell section. Returns: str: The exported cell """ return ( f"section Cell3DOS {self.tag} {self.area:.8e}" f" {self.omega:.8e} {self.py:.8e} {self.pz:.8e}" f" {self.material}" f" {self.y:.8e} {self.z:.8e}" )
[docs] def to_fibre_section( obj: Geometry | CompoundGeometry | Section, *, main_section_tag: int = 1, analysis_type: str = "3DOS", material_mapping: dict[str, int] | None = None, max_width: int = 160, save_to: str | None = None, ) -> str: """Export a section to the corresponding commands to create a fibre section. For a given geometry, this function computes necessary sectional properties and exports the corresponding commands to create a fibre/composite section that can be used in `suanPan <https://github.com/TLCFEM/suanPan>`_. Args: obj: The geometry/section to be exported main_section_tag: The tag of the main section analysis_type: The type of analysis would be performed material_mapping: A dictionary mapping material names to material tags max_width: The maximum width of a line in the output save_to: The path to save the output to Raises: TypeError: If `obj` is not a Geometry or Section ValueError: If `analysis_type` is not 2D, 3D or 3DOS Returns: str: The exported commands """ if isinstance(obj, (Geometry, CompoundGeometry)): geometry = obj elif isinstance(obj, Section): geometry = obj.geometry else: raise TypeError(f"Expected a Geometry or Section, got {type(obj).__name__}") analysis_type = analysis_type.upper() cell_class: type if analysis_type == "2D": cell_class = Cell2D fibre_class = "Fibre2D" elif analysis_type == "3D": cell_class = Cell3D fibre_class = "Fibre3D" elif analysis_type == "3DOS": cell_class = Cell3DOS fibre_class = "Fibre3DOS" else: raise ValueError("Invalid analysis type, expected 2D, 3D or 3DOS") section = Section(geometry) if cell_class is Cell3DOS: stiffness, force = section.assemble_torsion() omega = solver.solve_direct_lagrange(stiffness, force) else: omega = None cells = [cell_class(ele, omega) for ele in section.elements] commands = """# This is generated by sectionproperties library. # Please note the following: # 1. The warping function and its derivatives (if present) are computed in the # local coordinate system (about origin). # 2. Beware of the potential different orientations of beam section. # 3. It may be necessary to manually adjust the material tags. # 4. If uncertain, please validate the behaviour first. """ part = f"section {fibre_class} {main_section_tag} " for cell in cells: cell.tag += main_section_tag part += f"{cell.tag} " if len(part) > max_width: commands += part + "\\\n" part = "" commands += part + "\n\n" commands += "\n".join(cell.export() for cell in cells) commands += "\n\n" if material_mapping is not None: for name, tag in material_mapping.items(): commands = commands.replace("-".join(name.split()), str(tag)) if isinstance(save_to, str): with open(save_to, "w") as f: f.write(commands) return commands