#
##
## SPDX-FileCopyrightText: © 2007-2024 Benedict Verhegghe <bverheg@gmail.com>
## SPDX-License-Identifier: GPL-3.0-or-later
##
## This file is part of pyFormex 3.5 (Thu Feb 8 19:11:13 CET 2024)
## pyFormex is a tool for generating, manipulating and transforming 3D
## geometrical models by sequences of mathematical operations.
## Home page: https://pyformex.org
## Project page: https://savannah.nongnu.org/projects/pyformex/
## Development: https://gitlab.com/bverheg/pyformex
## Distributed under the GNU General Public License version 3 or later.
##
## This program 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.
##
## This program 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 this program. If not, see http://www.gnu.org/licenses/.
##
"""Utility functions for finite elements applications
This module contains some functions, data and classes for assist
finite element modelling.
You need to import this module in your scripts to have access to its
contents.
"""
from pyformex.arraytools import checkArray
import numpy as np
# Note: this could be combined with convertUnits function from
# plugins.units to do things like this:
#
# mat = IsotropicElasticity(E='21GPa',K='7000kN/cm**2')
# mat.E('Mpa') # would give value in MPa
#
[docs]class IsotropicElasticity():
"""Material constants for an isotropic linear elastic material.
Exactly 2 out of the following need to be specified:
E, G, nu, K, lmbda, mu, D
"""
def __init__(self, E=None, G=None, nu=None, K=None, lmbda=None, mu=None,
D=None):
# convert None values to float
E, G, nu, K, lmbda, mu, D = [float(var) if var is not None else None
for var in (E, G, nu, K, lmbda, mu, D)]
if G is None and mu is not None:
G = mu
if K is None and D is not None:
K = 2./D
# Set the young modulus E (checked for combinations with K, lamba, G)
if K is not None:
if lmbda is not None:
E = (9.*K*(K-lmbda))/(3.*K-lmbda)
if G is not None:
E=(9.*K*G)/(3.*K+G)
if nu is not None:
E = 3.*K*(1.-2.*nu)
if lmbda is not None:
if G is not None:
E = (G* (3.*lmbda + 2.*G))/(lmbda + G)
if nu is not None:
E = (lmbda*(1.+nu)*(1-2*nu))/nu
if G is not None:
if nu is not None:
E = 2.*G*(1.+nu)
# After setting the young modulus E computes G
# only the few checks below are needed
if lmbda is not None:
R = (E**2.+9.*lmbda**2.+2.*E*lmbda)**0.5
G = (E-3.*lmbda+R)/4.
if nu is not None:
G = E / (2*(1+nu))
if K is not None:
G = (3.*K*E)/(9*K-E)
self._E, self._G = E, G
@property
def E(self):
return self._E
@property
def G(self):
return self._G
@property
def nu(self):
return self._E / (2*self._G) - 1.0
@property
def K(self):
return self._G * self._E / (3*(3*self._G-self._E))
@property
def D(self):
return 2./self.K
@property
def lmbda(self):
E, G = self._E, self._G
return G*(E-2*G) / (3*G-E)
@property
def mu(self):
return self._G
def __str__(self):
return (f"E={self.E}; G=mu={self.G}; nu={self.nu}; "
f"K={self.K}; lmbda={self.lmbda}; D={self.D}")
[docs]class UniaxialStrain():
"""Uniaxial finite deformation strain measures.
This class provides a way to store finite deformation strain measures
and to convert between different strain measure definitions.
Parameters:
- `data`: :term:`array_like` float (n,): strain values
- `type`: one of 'stretch', 'log', 'nominal', 'green', 'almansi'.
Defines the type of strain measure:
- 'stretch': stretch (ratio) or extension ratio,
- 'nominal': nominal or engineering strain,
- 'log': logarithmic or true strain,
- 'green': Green strain (values should be >= -0.5),
- 'almansi': Almansi strain (values should be <= 0.5).
The default is to interprete the data as nominal strains.
Internally, the data are stored as stretch values.
"""
def __init__(self, data, type):
"""Initialize the UniaxialStrain"""
data = checkArray(data, shape=(-1,), kind='f')
if type == 'nominal':
data = data + 1.
elif type == 'log':
data = np.exp(data)
elif type == 'green':
data = np.sqrt(2*data + 1.)
elif type == 'almansi':
data = 1. / np.sqrt(1. - 2*data)
elif type != 'stretch':
raise ValueError("Invalid strain type: %s" % type)
self.data = data
[docs] def stretch(self):
"""Return the strain data as stretch ratios"""
return self.data
[docs] def log(self):
"""Return the strain data as logarithmic (true) strains"""
return np.log(self.data)
[docs] def nominal(self):
"""Return the strain data as nominal (engineering) strains"""
return self.data - 1.
[docs] def green(self):
"""Return the strain data as Green strains"""
return 0.5 * (self.data * self.data - 1.0)
[docs] def almansi(self):
"""Return the strain data as Almansi strains"""
return 0.5 * (1. - 1. / (self.data * self.data))
[docs]class UniaxialStress():
"""Uniaxial finite deformation stress measures.
This class provides a way to store finite deformation stress measures
and to convert between different stress measure definitions.
Parameters:
- `data`: :term:`array_like` float (n,): stress values
- `type`: one of 'cauchy', 'nominal', 'pk2'.
Defines the type of stress measure:
- 'cauchy': Cauchy or true stress,
- 'nominal': nominal, engineering or first Piola-Kirchhoff stress,
- 'pk2': second Piola-Kirchhoff stress.
The default is to interprete the data as nominal stresses.
- `strain`: a :class:`UniaxialStrain` instance of matching size (n,),
or :term:`array_like` float (n,) with the strain values. In the latter case
also `straintype` should be specified.
- `straintype`: if `strain` is specified as a :class:`UniaxialStrain`
instance, this argument is not used. Else it specifies the strain type
(see :class:`UniaxialStrain`).
Internally, the data are stored as Cauchy stress.
"""
def __init__(self, data, type, strain, straintype=None):
"""Initialize the UniaxialStress"""
data = checkArray(data, shape=(-1,), kind='f')
if not isinstance(strain, UniaxialStrain):
strain = UniaxialStrain(strain, straintype)
stretch = strain.stretch()
if type == 'nominal':
data = data * stretch
elif type == 'pk2':
data = data * stretch**2
elif type != 'cauchy':
raise ValueError("Invalid stress type: %s" % type)
self.data = data
self.strain = strain
def cauchy(self):
return self.data
def nominal(self):
return self.data / self.strain.stretch()
def pk2(self):
return self.data / self.strain.stretch()**2
# maybe move this to the amplitude class?
[docs]def smoothAmp(a, n=50):
""" Compute a single abaqus smooth amplitude.
Parameters
----------
a: :term:`array_like` (2,2)
Initial and final (amplitude,time) pairs
n: int
Number of intervals for the time variable
"""
tc = np.linspace(a[0, 0], a[1, 0], n)
eps = (tc-a[0, 0])/ (a[1, 0]-a[0, 0])
amp = a[0, 1] + (a[1, 1]-a[0, 1]) * eps**3 * (10.-15.*eps+6.*eps**2.)
return np.column_stack([tc, amp])
[docs]def ampSequence(a, n=100, f=smoothAmp):
"""Compute a final amplitude from a sequence of amplitudes.
Parameters
----------
a: :term:`array_like` (namp,2)
Every row is a pair of subsequent (amplitude,time) values
n: int
Number of of intervals for the time variable
f: callable
Amplitude function
"""
amp = np.empty((0, 2))
for i in range(len(a)-1):
amp = np.concatenate([amp, f(a[i:i+2], n=n)], axis=0)
return amp
[docs]def transverseShear(E, nu, c, type='generalized'):
""" Compute the shear stiffness for beam and shell element theory
For homogeneous beams/shells made of a linear, orthotropic elastic material.
Parameters
----------
E: float (1 or 2)
Elastic modulus in the one or two directions. If only one given,
the values in each directions are taken the same.
nu: float
Poisson modulus
c: float
Characteristic dimension. For beam elements it is the cross-
sectional area, for shell elements the thickness of the element.
type: str
Either 'shell' or one of the allowed beam cross section names as
defined in abaqus.
Returns
-------
list
A list of 3 values for the transverse shear.
For `type` == 'shell':
- shear stiffness in the 1st direction,
- shear stiffness in the 2nd direction,
- coupling term, assumed to be 0.
In all other cases (beam sections):
- shear stiffness,
- shear stiffness,
- slenderness compensation factor
See abaqus documentation for meanings and usage.
"""
k = {
'arbitrary': 1.0,
'box': 0.44,
'circular': 0.89,
'elbow': 0.85,
'generalized': 1.0,
'hexagonal': 0.53,
'i': 0.44,
't': 0.44,
'l': 1.0,
'meshed': 1.0,
'nonlinear': 1.0,
'pipe': 0.53,
'rectangular': 0.85,
'thick pipe1': 0.53,
'thick pipe2': 0.89,
'trapezoidal': 0.822,
'shell': 5/6.,
}
if np.asarray(E).size == 1:
E = [E, E]
sh = []
for em in E:
G = IsotropicElasticity(E=em, nu=nu).G
sh.append(k[type]*c*G) # Actual transverse shear stiffness
if type != 'shell':
# default slenderness compensation factor is only for beam elements
sh.append(k[type]/(2.*(1.+nu)))
else:
sh.append(0.)
return sh
# End