Source code for felupe.mechanics._truss

# -*- coding: utf-8 -*-
"""
This file is part of FElupe.

FElupe is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

FElupe is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with FElupe.  If not, see <http://www.gnu.org/licenses/>.
"""


import inspect

import numpy as np

from ..assembly import IntegralForm
from ..math import dot, dya, identity, sqrt
from ._helpers import Assemble, Evaluate, Results
from ._solidbody import Solid


[docs] class TrussBody(Solid): r"""A Truss body with methods for the assembly of sparse vectors/matrices. Parameters ---------- umat : class A class which provides methods for evaluating the gradient and the hessian of the strain energy density function per unit undeformed volume. The function signatures must be ``dψdλ, ζ_new = umat.gradient([λ, ζ])`` for the gradient and ``d2ψdλdλ = umat.hessian([λ, ζ])`` for the hessian of the strain energy density function :math:`\psi(\lambda)`, where :math:`\lambda` is the stretch and :math:`\zeta` holds the array of internal state variables. field : FieldContainer A field container with one or more fields. area : float or ndarray The cross-sectional areas of the trusses. statevars : ndarray or None, optional Array of initial internal state variables (default is None). Notes ----- For a truss element the stretch is evaluated as given in Eq. :eq:`truss-stretch` .. math:: :label: truss-stretch \Lambda = \sqrt{\frac{l^2}{L^2}} with the squared undeformed and deformed lengths, denoted in Eqs. :eq:`truss-lengths`. .. math:: :label: truss-lengths l^2 &= \boldsymbol{\Delta x}^T \boldsymbol{\Delta x} \\ L^2 &= \boldsymbol{\Delta X}^T \boldsymbol{\Delta X} This enables the Biot strain measure, see Eq. :eq:`truss-strain`. .. math:: :label: truss-strain E_{11} = \Lambda - 1 The normal force of a truss depends directly on the geometric exactly defined strain measure :math:`E_{11}`. For the general case of a nonlinear material behaviour the normal force is defined as given in Eq. :eq:`truss-force` .. math:: :label: truss-force N = S_{11}(E_{11})~A and the derivative according to Eq. :eq:`truss-force-derivative`. .. math:: :label: truss-force-derivative \frac{\partial N}{\partial E_{11}} = \frac{ \partial S_{11}(E_{11}) }{\partial E_{11}}~A For the case of a linear elastic material this reduces to Eqs. :eq:`truss-force-linear`. .. math:: :label: truss-force-linear S_{11}(E_{11}) &= E~E_{11} \\ N &= EA~E_{11} \\ \frac{\partial N}{\partial E_{11}} &= EA The (nonlinear) fixing force column vector may be expressed as a function of the elemental force :math:`N_k` and the deformed unit vector :math:`\boldsymbol{n}_k`, see Eq. :eq:`truss-fixing-force`. .. math:: :label: truss-fixing-force \boldsymbol{r}_k = \begin{bmatrix} \boldsymbol{r}_A \\ \boldsymbol{r}_E \end{bmatrix} = N_k \begin{pmatrix} -\boldsymbol{n}_k \\ \phantom{-}\boldsymbol{n}_k \end{pmatrix} For a truss the stiffness matrix is divided into four block matrices of the same components but with different signs, see Eq. :eq:`truss-stiffness-matrix`. .. math:: :label: truss-stiffness-matrix \boldsymbol{K}_{k~(6,6)} = \begin{bmatrix} \boldsymbol{K}_{AA} & \boldsymbol{K}_{AE}\\ \boldsymbol{K}_{EA} & \boldsymbol{K}_{EE} \end{bmatrix} = \begin{pmatrix} \phantom{-}\boldsymbol{K}_{EE} & -\boldsymbol{K}_{EE}\\ -\boldsymbol{K}_{EE} & \phantom{-}\boldsymbol{K}_{EE} \end{pmatrix} A change in the fixing force vector at the end node `E` w.r.t. a small change of the displacements at node `E` is defined as the tangent stiffnes `EE`, see Eq. :eq:`truss-stiffness-block`. .. math:: :label: truss-stiffness-block \boldsymbol{K}_{EE} &= \frac{ \partial \boldsymbol{r}_E}{\partial \boldsymbol{U}_E } \\ \boldsymbol{K}_{EE} &= \frac{A}{L} ~ \frac{ \partial S_{11}(E_{11}) }{\partial E_{11}} \boldsymbol{n} \otimes \boldsymbol{n} + \frac{N}{l} \left( \boldsymbol{1} - \boldsymbol{n} \otimes \boldsymbol{n} \right) Examples -------- .. pyvista-plot:: :context: :force_static: >>> import felupe as fem >>> >>> mesh = fem.Mesh( ... points=[[0, 0], [1, 1], [2.0, 0]], ... cells=[[0, 1], [1, 2]], ... cell_type="line", ... ) >>> region = fem.RegionTruss(mesh) >>> field = fem.Field(region, dim=2).as_container() >>> boundaries = fem.BoundaryDict(fixed=fem.Boundary(field[0], fy=0)) >>> >>> umat = fem.LinearElastic1D(E=[1, 1]) >>> truss = fem.TrussBody(umat, field, area=[1, 1]) >>> load = fem.PointLoad(field, points=[1]) >>> >>> table = fem.math.linsteps([0, 1], num=5, axis=1, axes=2) >>> ramp = {load: table * -0.1} >>> step = fem.Step(items=[truss, load], ramp=ramp, boundaries=boundaries) >>> job = fem.Job(steps=[step]).evaluate() >>> >>> mesh.plot( ... plotter=load.plot(plotter=boundaries.plot(), scale=0.5), ... line_width=5 ... ).show() """ def __init__(self, umat, field, area, statevars=None): self.field = field self.assemble = Assemble(vector=self._vector, matrix=self._matrix) self.evaluate = Evaluate(gradient=self._gradient, hessian=self._hessian) self.results = Results(stress=True, elasticity=True) self.umat = umat self.area = np.array(area) self.form_vector = IntegralForm( [None], v=self.field, dV=None, grad_v=[False], ) self.form_matrix = IntegralForm( [None], v=self.field, dV=None, u=self.field, grad_v=[False], grad_u=[False], ) if statevars is not None: self.results.statevars = statevars else: statevars_shape = (0,) if hasattr(umat, "x"): statevars_shape = umat.x[-1].shape self.results.statevars = np.zeros( ( *statevars_shape, field.region.mesh.ncells, ) ) cells = self.field.region.mesh.cells self.X = self.field.region.mesh.points[cells].T dX = self.X[:, 1] - self.X[:, 0] self.length_undeformed = sqrt(dot(dX, dX, mode=(1, 1))) def _kinematics(self, field): cells = self.field.region.mesh.cells u = field[0].values[cells].T x = self.X + u dx = x[:, 1] - x[:, 0] length_deformed = sqrt(dot(dx, dx, mode=(1, 1))) stretch = length_deformed / self.length_undeformed normal_deformed = dx / length_deformed return stretch, length_deformed, normal_deformed def _gradient(self, field=None, args=(), kwargs=None): if kwargs is None: kwargs = {} if field is not None: self.field = field self.results.kinematics = self._kinematics(field) if "out" in inspect.signature(self.umat.gradient).parameters: kwargs["out"] = self.results.gradient gradient = self.umat.gradient( [*self.results.kinematics, self.results.statevars], *args, **kwargs ) self.results.gradient = self.results.stress = gradient[0] self.results._statevars = gradient[-1] return self.results.gradient def _hessian(self, field=None, args=(), kwargs=None): if kwargs is None: kwargs = {} if field is not None: self.field = field self.results.kinematics = self._kinematics(field) if "out" in inspect.signature(self.umat.hessian).parameters: kwargs["out"] = self.results.hessian hessian = self.umat.hessian( [*self.results.kinematics, self.results.statevars], *args, **kwargs ) self.results.hessian = self.results.elasticity = hessian[0] return self.results.hessian def _vector(self, field=None, parallel=None, args=(), kwargs=None): self.results.stress = self._gradient(field, args=args, kwargs=kwargs) normal_deformed = self.results.kinematics[2] force = self.results.stress * self.area r = force * np.array([-normal_deformed, normal_deformed]) # (a, i, cell) self.results.force = self.form_vector.assemble(values=[r], parallel=parallel) return self.results.force def _matrix(self, field=None, parallel=None, args=(), kwargs=None): self.results.hessian = self._hessian(field, args=args, kwargs=kwargs) L = self.length_undeformed stretch, l, n = self.results.kinematics S = self.results.stress = self._gradient(field, args=args, kwargs=kwargs) dSdE = self.results.elasticity = self._hessian(field, args=args, kwargs=kwargs) m = dya(n, n, mode=1) eye = identity(dim=len(n), shape=(1,)) K_EE = dSdE / L * self.area * m + S / l * self.area * (eye - m) K = np.stack([[K_EE, -K_EE], [-K_EE, K_EE]]) # (a, b, i, j, cell) self.results.stiffness = self.form_matrix.assemble( values=[K.transpose([0, 2, 1, 3, 4])], parallel=parallel ) return self.results.stiffness