Source code for planestress.analysis.plane_stress

"""Class for a planestress analysis."""

from __future__ import annotations

from typing import TYPE_CHECKING

import numpy as np
import numpy.typing as npt
import scipy.sparse as sp_sparse

import planestress.analysis.solver as solver
from planestress.analysis.utils import dof_map
from planestress.post.results import Results


if TYPE_CHECKING:
    from planestress.pre.analysis_case import AnalysisCase
    from planestress.pre.geometry import Geometry
    from planestress.pre.mesh import Mesh


[docs] class PlaneStress: """Class for a plane-stress analysis. Attributes: geometry: ``Geometry`` object containing a meshed geometry. analysis_cases: List of analysis cases to analyse. mesh: ``Mesh`` object. """
[docs] def __init__( self, geometry: Geometry, analysis_cases: list[AnalysisCase], ) -> None: """Inits the PlaneStress class. Args: geometry: ``Geometry`` object containing a meshed geometry. analysis_cases: List of analysis cases to analyse. Raises: RuntimeError: If there is no mesh in the ``Geometry`` object. """ self.geometry = geometry self.analysis_cases = analysis_cases # check mesh has been created if len(self.geometry.mesh.nodes) < 1: raise RuntimeError( "No mesh detected, run Geometry.create_mesh() before creating a " "PlaneStress object." ) self.mesh: Mesh = self.geometry.mesh # reset boundary conditions mesh tags for analysis_case in self.analysis_cases: analysis_case.reset_mesh_tags() # assign mesh tags to boundary conditions for analysis_case in self.analysis_cases: analysis_case.assign_mesh_tags(mesh=self.mesh)
[docs] def solve( self, solver_type: str = "direct", ) -> list[Results]: """Solves each analysis case. Args: solver_type: Solver type, either ``"direct"`` (SciPy SuperLU sparse solver) or ``"pardiso"`` (Intel oneAPI Math Kernel Library PARDISO solver). Defaults to ``"direct"``. Raises: ValueError: If ``solver_type`` is not ``"direct"`` or ``"pardiso"``. Returns: A list of ``Results`` objects for post-processing corresponding to each analysis case. """ # get number of degrees of freedom num_dofs = self.mesh.num_nodes() * 2 # allocate results results: list[Results] = [] row_idxs: list[int] = [] col_idxs: list[int] = [] k_list: list[float] = [] # assemble stiffness matrix for el in self.mesh.elements: row_idxs.extend(el.element_row_indexes()) col_idxs.extend(el.element_col_indexes()) k_list.extend(el.element_stiffness_matrix()) # construct sparse matrix in COOrdinate format and convert to list of lists k = sp_sparse.coo_array( arg1=(k_list, (row_idxs, col_idxs)), shape=(num_dofs, num_dofs), dtype=np.float64, ).tolil() # for each analysis case for case in self.analysis_cases: # initialise modified stiffness matrix k_mod: sp_sparse.lil_array = k.copy() # initialise load vector f = np.zeros(num_dofs) f_app: npt.NDArray[np.float64] | None = None # assemble load vector for el in self.mesh.elements: # get element load vector f_el = el.element_load_vector( acceleration_field=case.acceleration_field ) # get element degrees of freedom el_dofs = dof_map(node_idxs=tuple(el.node_idxs)) # add element load vector to global load vector f[el_dofs] += f_el # apply boundary conditions # note these are sorted (load -> spring -> support) for boundary_condition in case.boundary_conditions: # check to see if we have finished applying external loads if boundary_condition.priority > 0 and f_app is None: f_app = np.copy(f) # apply boundary condition k_mod, f = boundary_condition.apply_bc(k=k_mod, f=f) # ensure f_app has been generated if f_app is None: f_app = f # solve system if solver_type == "direct": u = solver.solve_direct(k=k_mod, f=f) elif solver_type == "pardiso": u = solver.solve_pardiso(k=k_mod, f=f) else: raise ValueError( f"'solver_type' must be 'direct' or 'pardiso', not {solver_type}." ) # post-processing res = Results(plane_stress=self, u=u) res.calculate_node_forces(k=k) res.calculate_reactions(f=f_app) res.calculate_stresses(elements=self.mesh.elements) # add to results list results.append(res) return results