# Copyright (C) 2021-2025 Université Gustave Eiffel.
# This file is part of the EasyFEA project.
# EasyFEA is distributed under the terms of the GNU General Public License v3, see LICENSE.txt and CREDITS.md for more information.
"""Linearized elastic laws."""
from abc import ABC, abstractmethod
from typing import Union, Optional
from scipy.linalg import sqrtm
# utilities
import numpy as np
# others
from ..geoms import AsCoords, Normalize
from ._utils import (
_IModel,
ModelType,
Heterogeneous_Array,
KelvinMandel_Matrix,
Project_Kelvin,
Get_Pmat,
Apply_Pmat,
)
from ..utilities import _params, _types
from ..fem._linalg import TensorProd
# ----------------------------------------------
# Elasticity
# ----------------------------------------------
class _Elas(_IModel, ABC):
"""Linearized Elasticity material.\n
ElasIsot, ElasIsotTrans and ElasAnisot inherit from _Elas class.
"""
def __init__(self, dim: int, thickness: float, planeStress: bool):
self.dim = dim
self.planeStress = planeStress
self.thickness = thickness
@property
def modelType(self) -> ModelType:
return ModelType.elastic
dim: int = _params.ParameterInValues([2, 3])
thickness: float = _params.PositiveScalarParameter()
planeStress: bool = _params.BoolParameter()
"""the model uses plane stress simplification"""
@property
def simplification(self) -> str:
"""simplification used for the model"""
if self.dim == 2:
return "Plane Stress" if self.planeStress else "Plane Strain"
else:
return "3D"
@abstractmethod
def _Update(self) -> None:
"""Updates the constitutives laws by updating the C stiffness and S compliance matrices. in Kelvin Mandel notation"""
pass
# Model
@staticmethod
def Available_Laws():
laws = [ElasIsot, ElasIsotTrans, ElasAnisot]
return laws
@property
def coef(self) -> float:
"""kelvin mandel coef -> sqrt(2)"""
return np.sqrt(2)
@property
def C(self) -> _types.FloatArray:
"""Stifness matrix in Kelvin Mandel notation such that:\n
In 2D: C -> C: Epsilon = Sigma [Sxx, Syy, sqrt(2)*Sxy]\n
In 3D: C -> C: Epsilon = Sigma [Sxx, Syy, Szz, sqrt(2)*Syz, sqrt(2)*Sxz, sqrt(2)*Sxy].\n
(Lame's law)
"""
if self.needUpdate:
self._Update()
self.Need_Update(False)
return self.__C.copy()
# 23 Cannot be a descriptor due to conflict with `__sqrt_C`.
@C.setter
def C(self, array: _types.FloatArray):
assert isinstance(array, np.ndarray), "must be an array"
shape = (3, 3) if self.dim == 2 else (6, 6)
assert (
array.shape[-2:] == shape
), f"With dim = {self.dim} array must be a {shape} matrix"
self.__C = array
self.__sqrt_C = None # dont remove
@property
def isHeterogeneous(self) -> bool:
return len(self.C.shape) > 2
# 23 Cannot be a descriptor due to conflict with `__sqrt_S`.
@property
def S(self) -> _types.FloatArray:
"""Compliance matrix in Kelvin Mandel notation such that:\n
In 2D: S -> S : Sigma = Epsilon [Exx, Eyy, sqrt(2)*Exy]\n
In 3D: S -> S: Sigma = Epsilon [Exx, Eyy, Ezz, sqrt(2)*Eyz, sqrt(2)*Exz, sqrt(2)*Exy].\n
(Hooke's law)
"""
if self.needUpdate:
self._Update()
self.Need_Update(False)
return self.__S.copy()
@S.setter
def S(self, array: _types.FloatArray):
assert isinstance(array, np.ndarray), "must be an array"
shape = (3, 3) if self.dim == 2 else (6, 6)
assert (
array.shape[-2:] == shape
), f"With dim = {self.dim} array must be a {shape} matrix"
self.__S = array
self.__sqrt_S = None # dont remove
@abstractmethod
def Walpole_Decomposition(self) -> tuple[_types.FloatArray, _types.FloatArray]:
"""Walpole's decomposition in Kelvin Mandel notation such that:\n
C = sum(ci * Ei).\n
returns ci, Ei"""
return np.array([]), np.array([])
def Get_sqrt_C_S(self) -> tuple[_types.FloatArray, _types.FloatArray]:
"""Returns the Matrix square root of C and S."""
C = self.C
try:
self.__sqrt_C is None
self.__sqrt_S is None
except AttributeError:
# init
self.__sqrt_C: Optional[_types.FloatArray] = None # type: ignore [no-redef]
self.__sqrt_S: Optional[_types.FloatArray] = None # type: ignore [no-redef]
if self.__sqrt_C is None:
if self.isHeterogeneous:
shape = C.shape
assert (
len(shape) == 3
), "This function is not currently implemented for heterogeneous matrices where material properties are defined on Gauss points."
uniq_C, inverse = np.unique(C, return_inverse=True, axis=0)
sqrtC = np.zeros_like(C, dtype=float)
sqrtS = np.zeros_like(C, dtype=float)
for i, C in enumerate(uniq_C):
elems = np.where(inverse == i)[0]
sqrtmC = sqrtm(C)
sqrtC[elems] = sqrtmC
sqrtmS = np.linalg.inv(sqrtmC)
sqrtS[elems] = sqrtmS
else:
sqrtC = sqrtm(C)
sqrtS = np.linalg.inv(sqrtC) # faster than sqrtm(self.S)
# sqrtS = sqrtm(self.S)
# # give the same results !!!
# test = np.linalg.norm(sqrtS - sqrtm(self.S))/np.linalg.norm(sqrtS)
# assert test < 1e-12
self.__sqrt_C = sqrtC # type: ignore [assignment]
self.__sqrt_S = sqrtS # type: ignore [assignment]
else:
sqrtC = self.__sqrt_C.copy()
sqrtS = self.__sqrt_S.copy()
return sqrtC, sqrtS
def _Apply_basis_transformation(
self,
dim: int,
material_cM: _types.FloatArray,
material_sM: _types.FloatArray,
axis_1: _types.FloatArray,
axis_2: _types.FloatArray,
) -> tuple[_types.FloatArray, _types.FloatArray]:
"""Performs a basis transformation from the material's (1,2,3) coordinate system to the (x,y,z) coordinate system to orient the material in space.
Parameters
----------
dim : int
dimension
material_cM : _types.FloatArray
stiffness matrix
material_sM : _types.FloatArray
compliance matrix
axis_1 : _types.FloatArray
Axis 1
axis_2 : _types.FloatArray
Axis 2
Returns
-------
tuple[_types.FloatArray, _types.FloatArray]
global_cM, global_sM
"""
P = Get_Pmat(axis_1=axis_1, axis_2=axis_2, useMandel=True)
global_sM = Apply_Pmat(P, material_sM, toGlobal=True)
global_cM = Apply_Pmat(P, material_cM, toGlobal=True)
testAxis_1 = np.linalg.norm(axis_1 - np.array([1, 0, 0])) <= 1e-12
testAxis_2 = np.linalg.norm(axis_2 - np.array([0, 1, 0])) <= 1e-12
if testAxis_1 and testAxis_2:
# check that if the axes does not change, the same constitutive law is obtained
test_diff_c = np.linalg.norm(
global_cM - material_cM, axis=(-2, -1)
) / np.linalg.norm(material_cM, axis=(-2, -1))
assert np.max(test_diff_c) < 1e-12
test_diff_s = np.linalg.norm(
global_sM - material_sM, axis=(-2, -1)
) / np.linalg.norm(material_sM, axis=(-2, -1))
assert np.max(test_diff_s) < 1e-12
c = global_cM
s = global_sM
if dim == 2:
x = np.array([0, 1, 5])
shape = c.shape
if self.planeStress:
if len(shape) == 2:
s = global_sM[x, :][:, x]
elif len(shape) == 3:
s = global_sM[:, x, :][:, :, x]
elif len(shape) == 4:
s = global_sM[:, :, x, :][:, :, :, x]
c = np.linalg.inv(s)
else:
if len(shape) == 2:
c = global_cM[x, :][:, x]
elif len(shape) == 3:
c = global_cM[:, x, :][:, :, x]
elif len(shape) == 4:
c = global_cM[:, :, x, :][:, :, :, x]
s = np.linalg.inv(c)
return c, s
# ----------------------------------------------
# Isotropic
# ----------------------------------------------
[docs]
class ElasIsot(_Elas):
"""Isotropic Linearized Elastic material."""
E: float = _params.PositiveParameter()
"""Young's modulus"""
v: float = _params.IntervalccParameter(inf=-1, sup=0.5)
"""Poisson's ratio (-1<v<0.5)"""
def __str__(self) -> str:
text = f"{type(self).__name__}:"
text += f"\nE = {self.E:.2e}, v = {self.v}"
if self.dim == 2:
text += f"\nplaneStress = {self.planeStress}"
text += f"\nthickness = {self.thickness:.2e}"
return text
def __init__(self, dim: int, E=210000.0, v=0.3, planeStress=True, thickness=1.0):
"""Creates an Isotropic Linearized Elastic material.
Parameters
----------
dim : int
dimension (e.g 2 or 3)
E : float|np.ndarray, optional
Young's modulus
v : float|np.ndarray, optional
Poisson's ratio ]-1;0.5]
planeStress : bool, optional
uses plane stress assumption, by default True
thickness : float, optional
thickness, by default 1.0
"""
_Elas.__init__(self, dim, thickness, planeStress)
self.E = E
self.v = v
def _Update(self) -> None:
C, S = self._Behavior(self.dim)
self.C = C
self.S = S
[docs]
def get_lambda(self):
E = self.E
v = self.v
lmbda = E * v / ((1 + v) * (1 - 2 * v))
if self.dim == 2 and self.planeStress:
lmbda = E * v / (1 - v**2)
return lmbda
[docs]
def get_mu(self):
"""Shear coefficient"""
E = self.E
v = self.v
mu = E / (2 * (1 + v))
return mu
[docs]
def get_bulk(self):
"""Bulk modulus"""
mu = self.get_mu()
lmbda = self.get_lambda()
bulk = lmbda + 2 * mu / self.dim
return bulk
def _Behavior(self, dim: Optional[int] = None):
"""Updates the constitutives laws by updating the C stiffness and S compliance matrices in Kelvin Mandel notation.\n
In 2D:
-----
C -> C : Epsilon = Sigma [Sxx Syy sqrt(2)*Sxy]\n
S -> S : Sigma = Epsilon [Exx Eyy sqrt(2)*Exy]
In 3D:
-----
C -> C : Epsilon = Sigma [Sxx Syy Szz sqrt(2)*Syz sqrt(2)*Sxz sqrt(2)*Sxy]\n
S -> S : Sigma = Epsilon [Exx Eyy Ezz sqrt(2)*Eyz sqrt(2)*Exz sqrt(2)*Exy]
"""
if dim is None:
dim = self.dim
else:
assert dim in [2, 3]
E = self.E
v = self.v
mu = self.get_mu()
lmbda = self.get_lambda()
dtype = object if True in [isinstance(p, np.ndarray) for p in [E, v]] else float
if dim == 2:
# Caution: lambda changes according to 2D simplification.
cVoigt = np.array(
[[lmbda + 2 * mu, lmbda, 0], [lmbda, lmbda + 2 * mu, 0], [0, 0, mu]],
dtype=dtype,
)
# if self.contraintesPlanes:
# # C = np.array([ [4*(mu+l), 2*l, 0],
# # [2*l, 4*(mu+l), 0],
# # [0, 0, 2*mu+l]]) * mu/(2*mu+l)
# cVoigt = np.array([ [1, v, 0],
# [v, 1, 0],
# [0, 0, (1-v)/2]]) * E/(1-v**2)
# else:
# cVoigt = np.array([ [l + 2*mu, l, 0],
# [l, l + 2*mu, 0],
# [0, 0, mu]])
# # C = np.array([ [1, v/(1-v), 0],
# # [v/(1-v), 1, 0],
# # [0, 0, (1-2*v)/(2*(1-v))]]) * E*(1-v)/((1+v)*(1-2*v))
elif dim == 3:
cVoigt = np.array(
[
[lmbda + 2 * mu, lmbda, lmbda, 0, 0, 0],
[lmbda, lmbda + 2 * mu, lmbda, 0, 0, 0],
[lmbda, lmbda, lmbda + 2 * mu, 0, 0, 0],
[0, 0, 0, mu, 0, 0],
[0, 0, 0, 0, mu, 0],
[0, 0, 0, 0, 0, mu],
],
dtype=dtype,
)
cVoigt = Heterogeneous_Array(cVoigt)
c = KelvinMandel_Matrix(dim, cVoigt)
s = np.linalg.inv(c)
return c, s
[docs]
def Walpole_Decomposition(self) -> tuple[_types.FloatArray, _types.FloatArray]:
c1 = self.get_bulk()
c2 = self.get_mu()
Ivect = np.array([1, 1, 1, 0, 0, 0])
Isym = np.eye(6)
E1 = 1 / 3 * TensorProd(Ivect, Ivect)
E2 = Isym - E1
if not self.isHeterogeneous:
C = self.C
# only test if the material is heterogeneous
test_C = np.linalg.norm((3 * c1 * E1 + 2 * c2 * E2) - C) / np.linalg.norm(C)
assert test_C <= 1e-12
ci = np.array([c1, c2])
Ei = np.array([3 * E1, 2 * E2])
return ci, Ei
# ----------------------------------------------
# Transversely isotropic
# ----------------------------------------------
[docs]
class ElasIsotTrans(_Elas):
"""Transversely Isotropic Linearized Elastic material."""
El: float = _params.PositiveParameter()
"""Longitudinal Young's modulus."""
Et: float = _params.PositiveParameter()
"""Transverse Young's modulus."""
Gl: float = _params.PositiveParameter()
"""Longitudinal shear modulus."""
vl: float = _params.IntervalccParameter(inf=-1, sup=0.5)
"""Longitudinal Poisson's ratio (-1<vl<0.5)."""
vt: float = _params.IntervalccParameter(inf=-1, sup=1)
"""Transverse Poisson ratio (-1<vt<1)"""
def __str__(self) -> str:
text = f"{type(self).__name__}:"
text += f"\nEl = {self.El:.2e}, Et = {self.Et:.2e}, Gl = {self.Gl:.2e}"
text += f"\nvl = {self.vl}, vt = {self.vt}"
text += f"\naxis_l = {np.array_str(self.axis_l, precision=3)}"
text += f"\naxis_t = {np.array_str(self.axis_t, precision=3)}"
if self.dim == 2:
text += f"\nplaneStress = {self.planeStress}"
text += f"\nthickness = {self.thickness:.2e}"
return text
def __init__(
self,
dim: int,
El: float,
Et: float,
Gl: float,
vl: float,
vt: float,
axis_l: _types.Coords = (1, 0, 0),
axis_t: _types.Coords = (0, 1, 0),
planeStress: bool = True,
thickness: float = 1.0,
):
"""Creates an Transversely Isotropic Linearized Elastic material.\n
More details Torquato 2002 13.3.2 (iii) http://link.springer.com/10.1007/978-1-4757-6355-3
Parameters
----------
dim : int
Dimension of 2D or 3D simulation
El : float
Longitudinal Young's modulus
Et : float
Transverse Young's modulus (T, R) plane
Gl : float
Longitudinal shear modulus
vl : float
Longitudinal Poisson ratio
vt : float
Transverse Poisson ratio (T, R) plane
axis_l : _types.Coords, optional
Longitudinal axis, by default np.array([1,0,0])
axis_t : _types.Coords, optional
Transverse axis, by default np.array([0,1,0])
planeStress : bool, optional
uses plane stress assumption, by default True
thickness : float, optional
thickness, by default 1.0
"""
_Elas.__init__(self, dim, thickness, planeStress)
self.El = El
self.Et = Et
self.Gl = Gl
self.vl = vl
self.vt = vt
axis_l = AsCoords(axis_l)
axis_t = AsCoords(axis_t)
assert axis_l.size == 3 and len(axis_l.shape) == 1, "axis_l must be a 3D vector"
assert axis_t.size == 3 and len(axis_t.shape) == 1, "axis_t must be a 3D vector"
assert axis_l @ axis_t <= 1e-12, "axis1 and axis2 must be perpendicular"
self.__axis_l = Normalize(axis_l)
self.__axis_t = Normalize(axis_t)
@property
def Gt(self) -> Union[float, _types.FloatArray]:
"""Transverse shear modulus."""
Et = self.Et
vt = self.vt
Gt = Et / (2 * (1 + vt))
return Gt
@property
def kt(self) -> Union[float, _types.FloatArray]:
# Torquato 2002 13.3.2 (iii)
El = self.El
Et = self.Et
vtt = self.vt
vtl = self.vl
kt = El * Et / ((2 * (1 - vtt) * El) - (4 * vtl**2 * Et))
return kt
@property
def axis_l(self) -> _types.FloatArray:
"""Longitudinal axis"""
return self.__axis_l.copy()
@property
def axis_t(self) -> _types.FloatArray:
"""Transversal axis"""
return self.__axis_t.copy()
def _Update(self) -> None:
C, S = self._Behavior(self.dim)
self.C = C
self.S = S
def _Behavior(self, dim: Optional[int] = None):
"""Updates the constitutives laws by updating the C stiffness and S compliance matrices in Kelvin Mandel notation.\n
In 2D:
-----
C -> C : Epsilon = Sigma [Sxx Syy sqrt(2)*Sxy]\n
S -> S : Sigma = Epsilon [Exx Eyy sqrt(2)*Exy]
In 3D:
-----
C -> C : Epsilon = Sigma [Sxx Syy Szz sqrt(2)*Syz sqrt(2)*Sxz sqrt(2)*Sxy]\n
S -> S : Sigma = Epsilon [Exx Eyy Ezz sqrt(2)*Eyz sqrt(2)*Exz sqrt(2)*Exy]
"""
if dim is None:
dim = self.dim
El = self.El
Et = self.Et
vt = self.vt
vl = self.vl
Gl = self.Gl
Gt = self.Gt
kt = self.kt
dtype = object if isinstance(kt, np.ndarray) else float
# Kelvin-Mandel compliance and stiffness matrices in the material's coordinate system.
# L = (1, 0, 0)
# T = (0, 1, 0)
# R = (0, 0, 1)
# [11, 22, 33, sqrt(2)*23, sqrt(2)*13, sqrt(2)*12]
material_sM = np.array(
[
[1 / El, -vl / El, -vl / El, 0, 0, 0],
[-vl / El, 1 / Et, -vt / Et, 0, 0, 0],
[-vl / El, -vt / Et, 1 / Et, 0, 0, 0],
[0, 0, 0, 1 / (2 * Gt), 0, 0],
[0, 0, 0, 0, 1 / (2 * Gl), 0],
[0, 0, 0, 0, 0, 1 / (2 * Gl)],
],
dtype=dtype,
)
material_sM = Heterogeneous_Array(material_sM)
material_cM = np.array(
[
[El + 4 * vl**2 * kt, 2 * kt * vl, 2 * kt * vl, 0, 0, 0],
[2 * kt * vl, kt + Gt, kt - Gt, 0, 0, 0],
[2 * kt * vl, kt - Gt, kt + Gt, 0, 0, 0],
[0, 0, 0, 2 * Gt, 0, 0],
[0, 0, 0, 0, 2 * Gl, 0],
[0, 0, 0, 0, 0, 2 * Gl],
],
dtype=dtype,
)
material_cM = Heterogeneous_Array(material_cM)
if len(material_cM.shape) == 2:
# checks that S = C^-1
diff_S = np.linalg.norm(
material_sM - np.linalg.inv(material_cM), axis=(-2, -1)
) / np.linalg.norm(material_sM, axis=(-2, -1))
assert np.max(diff_S) < 1e-12
# checks that C = S^-1
diff_C = np.linalg.norm(
material_cM - np.linalg.inv(material_sM), axis=(-2, -1)
) / np.linalg.norm(material_cM, axis=(-2, -1))
assert np.max(diff_C) < 1e-12
return self._Apply_basis_transformation(
dim=dim,
material_cM=material_cM,
material_sM=material_sM,
axis_1=self.axis_l,
axis_2=self.axis_t,
)
[docs]
def Walpole_Decomposition(self) -> tuple[_types.FloatArray, _types.FloatArray]:
El = self.El
Gl = self.Gl
vl = self.vl
kt = self.kt
Gt = self.Gt
c1 = El + 4 * vl**2 * kt
c2 = 2 * kt
c3 = 2 * np.sqrt(2) * kt * vl
c4 = 2 * Gt
c5 = 2 * Gl
n = self.axis_l
p = TensorProd(n, n)
q = np.eye(3) - p
E1 = Project_Kelvin(TensorProd(p, p))
E2 = Project_Kelvin(1 / 2 * TensorProd(q, q))
E3 = Project_Kelvin(1 / np.sqrt(2) * (TensorProd(p, q) + TensorProd(q, p)))
E4 = Project_Kelvin(TensorProd(q, q, True) - 1 / 2 * TensorProd(q, q))
I = Project_Kelvin(TensorProd(np.eye(3), np.eye(3), True))
E5 = I - E1 - E2 - E4
if not self.isHeterogeneous:
C, S = self._Behavior(3)
diff_C = C - (c1 * E1 + c2 * E2 + c3 * E3 + c4 * E4 + c5 * E5)
test_C = np.linalg.norm(diff_C, axis=(-2, -1)) / np.linalg.norm(
C, axis=(-2, -1)
)
assert test_C < 1e-12
ci = np.array([c1, c2, c3, c4, c5])
Ei = np.array([E1, E2, E3, E4, E5])
return ci, Ei
# ----------------------------------------------
# Orthotropic
# ----------------------------------------------
class ElasOrthotropic(_Elas):
"""Orthotropic Linearized Elastic material."""
E1: float = _params.PositiveParameter()
"""Young's modulus along axis_1."""
E2: float = _params.PositiveParameter()
"""Young's modulus along axis_2."""
E3: float = _params.PositiveParameter()
"""Young's modulus along axis_3."""
G23: float = _params.PositiveParameter()
"""Shear modulus in the 2-3 plane."""
G13: float = _params.PositiveParameter()
"""Shear modulus in the 1-3 plane."""
G12: float = _params.PositiveParameter()
"""Shear modulus in the 1-2 plane."""
v23: float = _params.IntervalccParameter(inf=-1, sup=0.5)
"""Poisson's ratio for transverse strain along the axis_3 when stressed along the axis_2."""
v13: float = _params.IntervalccParameter(inf=-1, sup=0.5)
"""Poisson's ratio for transverse strain along the axis_3 when stressed along the axis_1."""
v12: float = _params.IntervalccParameter(inf=-1, sup=0.5)
"""Poisson's ratio for transverse strain along the axis_2 when stressed along the axis_1."""
def __str__(self) -> str:
text = f"{type(self).__name__}:"
text += f"\nE1 = {self.E1:.2e}"
text += f"\nE2 = {self.E2:.2e}"
text += f"\nE3 = {self.E3:.2e}"
text += f"\nG23 = {self.G23:.2e}"
text += f"\nG13 = {self.G13:.2e}"
text += f"\nG12 = {self.G12:.2e}"
text += f"\nv23 = {self.v23:.2e}"
text += f"\nv13 = {self.v13:.2e}"
text += f"\nv12 = {self.v12:.2e}"
text += f"\naxis_1 = {np.array_str(self.axis_1, precision=3)}"
text += f"\naxis_2 = {np.array_str(self.axis_1, precision=3)}"
if self.dim == 2:
text += f"\nplaneStress = {self.planeStress}"
text += f"\nthickness = {self.thickness:.2e}"
return text
def __init__(
self,
dim: int,
E1: float,
E2: float,
E3: float,
G23: float,
G13: float,
G12: float,
v23: float,
v13: float,
v12: float,
axis_1: _types.Coords = (1, 0, 0),
axis_2: _types.Coords = (0, 1, 0),
planeStress: bool = True,
thickness: float = 1.0,
):
"""Creates Orthotropic Linearized Elastic material.\n
More details https://www.lusas.com/user_area/faqs/orthotropic.html#:~:text=The%20inverse%20of%20the%20compliance,of%20both%20matrices%20are%20positive
Parameters
----------
dim : int
Dimension of 2D or 3D simulation
E1 : float
Young's modulus along axis_1.
E2 : float
Young's modulus along axis_2.
E3 : float
Young's modulus along axis_3.
G23 : float
Shear modulus in the 2-3 plane.
G13 : float
Shear modulus in the 1-3 plane.
G12 : float
Shear modulus in the 1-2 plane.
v23 : float
Poisson's ratio for transverse strain along the axis_3 when stressed along the axis_2.
v13 : float
Poisson's ratio for transverse strain along the axis_3 when stressed along the axis_1.
v12 : float
Poisson's ratio for transverse strain along the axis_2 when stressed along the axis_1.
axis_1 : _types.Coords, optional
Axis 1, by default np.array([1,0,0])
axis_t : _types.Coords, optional
Axis 2, by default np.array([0,1,0])
planeStress : bool, optional
uses plane stress assumption, by default True
thickness : float, optional
thickness, by default 1.0
"""
_Elas.__init__(self, dim, thickness, planeStress)
self.E1 = E1
self.E2 = E2
self.E3 = E3
self.G23 = G23
self.G13 = G13
self.G12 = G12
self.v23 = v23
self.v13 = v13
self.v12 = v12
axis_1 = AsCoords(axis_1)
axis_2 = AsCoords(axis_2)
assert axis_1.size == 3 and len(axis_1.shape) == 1, "axis_1 must be a 3D vector"
assert axis_2.size == 3 and len(axis_2.shape) == 1, "axis_2 must be a 3D vector"
assert axis_1 @ axis_2 <= 1e-12, "axis1 and axis2 must be perpendicular"
self.__axis_1 = Normalize(axis_1)
self.__axis_2 = Normalize(axis_2)
@property
def axis_1(self) -> _types.FloatArray:
"""Axis 1"""
return self.__axis_1.copy()
@property
def axis_2(self) -> _types.FloatArray:
"""Axis 2"""
return self.__axis_2.copy()
def _Update(self) -> None:
C, S = self._Behavior(self.dim)
self.C = C
self.S = S
def _Behavior(self, dim: Optional[int] = None):
"""Updates the constitutives laws by updating the C stiffness and S compliance matrices in Kelvin Mandel notation.\n
In 2D:
-----
C -> C : Epsilon = Sigma [Sxx Syy sqrt(2)*Sxy]\n
S -> S : Sigma = Epsilon [Exx Eyy sqrt(2)*Exy]
In 3D:
-----
C -> C : Epsilon = Sigma [Sxx Syy Szz sqrt(2)*Syz sqrt(2)*Sxz sqrt(2)*Sxy]\n
S -> S : Sigma = Epsilon [Exx Eyy Ezz sqrt(2)*Eyz sqrt(2)*Exz sqrt(2)*Exy]
"""
if dim is None:
dim = self.dim
E1 = self.E1
E2 = self.E2
E3 = self.E3
G23 = self.G23
G13 = self.G13
G12 = self.G12
v23 = self.v23
v13 = self.v13
v12 = self.v12
sum = E1 + E2 + E3 + G23 + G13 + G12 + v23 + v13 + v12
dtype = object if isinstance(sum, np.ndarray) else float
# Kelvin-Mandel compliance and stiffness matrices in the material's coordinate system.
# axis_1 = (1, 0, 0)
# axis_2 = (0, 1, 0)
# axis_3 = (0, 0, 1)
# [11, 22, 33, sqrt(2)*23, sqrt(2)*13, sqrt(2)*12]
material_sM = np.array(
[
[1 / E1, -v12 / E1, -v13 / E1, 0, 0, 0],
[-v12 / E1, 1 / E2, -v23 / E2, 0, 0, 0],
[-v13 / E1, -v23 / E2, 1 / E3, 0, 0, 0],
[0, 0, 0, 1 / (2 * G23), 0, 0],
[0, 0, 0, 0, 1 / (2 * G13), 0],
[0, 0, 0, 0, 0, 1 / (2 * G12)],
],
dtype=dtype,
)
# tests on S values
s11, s22, s33 = [material_sM[d, d] for d in range(3)]
s23, s13, s12 = material_sM[1, 2], material_sM[0, 2], material_sM[0, 1]
assert np.all(np.abs(s23) < np.sqrt(s22 * s33)), "|s23| < sqrt(s22 * s33)"
assert np.all(np.abs(s13) < np.sqrt(s11 * s33)), "|s13| < sqrt(s11 * s33)"
assert np.all(np.abs(s12) < np.sqrt(s11 * s22)), "|s12| < sqrt(s11 * s22)"
assert np.all(np.abs(v23) < np.sqrt(E2 / E3)), "|v23| < sqrt(E2 / E3)"
assert np.all(np.abs(v13) < np.sqrt(E1 / E3)), "|v13| < sqrt(E1 / E3)"
assert np.all(np.abs(v12) < np.sqrt(E1 / E2)), "|v12| < sqrt(E1 / E2)"
material_sM = Heterogeneous_Array(material_sM)
# get c matrix
if material_sM.ndim > 2:
material_cM = np.zeros_like(material_sM, dtype=float)
uniq_S, inverse = np.unique(material_sM, return_inverse=True, axis=0)
for i, S in enumerate(uniq_S):
elems = np.where(inverse == i)[0]
C = np.linalg.inv(S)
material_cM[elems] = C
else:
material_cM = np.linalg.inv(material_sM)
return self._Apply_basis_transformation(
dim=dim,
material_cM=material_cM,
material_sM=material_sM,
axis_1=self.axis_1,
axis_2=self.axis_2,
)
def Walpole_Decomposition(self):
return super().Walpole_Decomposition()
# ----------------------------------------------
# Anisotropic
# ----------------------------------------------
[docs]
class ElasAnisot(_Elas):
"""Anisotropic Linearized Elastic material."""
def __str__(self) -> str:
text = f"\n{type(self).__name__}):"
text += f"\n{self.C}"
text += f"\naxis1 = {np.array_str(self.__axis1, precision=3)}"
text += f"\naxis2 = {np.array_str(self.__axis2, precision=3)}"
if self.dim == 2:
text += f"\nplaneStress = {self.planeStress}"
text += f"\nthickness = {self.thickness:.2e}"
return text
def __init__(
self,
dim: int,
C: _types.FloatArray,
useVoigtNotation: bool,
axis1: _types.Coords = (1, 0, 0),
axis2: _types.Coords = (0, 1, 0),
thickness=1.0,
):
"""Creates an Anisotropic Linearized Elastic class.
Parameters
----------
dim : int
dimension
C : _types.FloatArray
stiffness matrix in anisotropy basis
useVoigtNotation : bool
behavior law uses voigt notation
axis1 : _types.Coords, optional
axis1 vector, by default (1,0,0)
axis2 : _types.Coords
axis2 vector, by default (0,1,0)
thickness: float, optional
material thickness, by default 1.0
Returns
-------
ElasAnisot
Anisotropic behavior law
"""
# here planeStress is set to False because we just know the C matrix
_Elas.__init__(self, dim, thickness, False)
axis1 = AsCoords(axis1)
axis2 = AsCoords(axis2)
assert axis1.size == 3 and len(axis1.shape) == 1, "axis1 must be a 3D vector"
assert axis2.size == 3 and len(axis2.shape) == 1, "axis2 must be a 3D vector"
assert axis1 @ axis2 <= 1e-12, "axis1 and axis2 must be perpendicular"
self.__axis1 = Normalize(axis1)
self.__axis2 = Normalize(axis2)
self.Set_C(C, useVoigtNotation)
def _Update(self) -> None:
# doesn't do anything here, because we use Set_C to update the laws.
return super()._Update()
[docs]
def Set_C(self, C: _types.FloatArray, useVoigtNotation=True, update_S=True):
"""Updates the constitutives laws by updating the C stiffness and S compliance matrices in Kelvin Mandel notation.\n
Parameters
----------
C : _types.FloatArray
Stifness matrix (Lamé's law)
useVoigtNotation : bool, optional
uses Kevin Mandel's notation, by default True
update_S : bool, optional
updates the compliance matrix (Hooke's law), by default True
"""
self.Need_Update()
C_mandelP = self._Behavior(C, useVoigtNotation)
self.C = C_mandelP
if update_S:
S_mandelP = np.linalg.inv(C_mandelP)
self.S = S_mandelP
def _Behavior(
self, C: _types.FloatArray, useVoigtNotation: bool
) -> _types.FloatArray:
shape = C.shape
assert (shape[-2], shape[-1]) in [
(3, 3),
(6, 6),
], "C must be a (3,3) or (6,6) matrix"
dim = 3 if C.shape[-1] == 6 else 2
if len(C.shape) == 2:
Ct = C.T
elif len(C.shape) == 3:
Ct = np.transpose(C, (0, 2, 1))
elif len(C.shape) == 4:
Ct = np.transpose(C, (0, 1, 3, 2))
else:
raise ValueError(
"This matrix must be of dimensions (dim, dim), (Ne, dim, dim) or (Ne, nPg, dim, dim)."
)
testSym = np.linalg.norm(Ct - C, axis=(-2, -1)) / np.linalg.norm(
C, axis=(-2, -1)
)
assert np.max(testSym) <= 1e-12, "The matrix is not symmetrical."
if useVoigtNotation:
C_mandel = KelvinMandel_Matrix(dim, C)
else:
C_mandel = C.copy()
# sets to 3D
idx = np.array([0, 1, 5])
if dim == 2:
if len(shape) == 2:
C_mandel_global = np.zeros((6, 6))
for i, I in enumerate(idx):
for j, J in enumerate(idx):
C_mandel_global[I, J] = C_mandel[i, j]
if len(shape) == 3:
C_mandel_global = np.zeros((shape[0], 6, 6))
for i, I in enumerate(idx):
for j, J in enumerate(idx):
C_mandel_global[:, I, J] = C_mandel[:, i, j]
elif len(shape) == 4:
C_mandel_global = np.zeros((shape[0], shape[1], 6, 6))
for i, I in enumerate(idx):
for j, J in enumerate(idx):
C_mandel_global[:, :, I, J] = C_mandel[:, :, i, j]
else:
C_mandel_global = C
P = Get_Pmat(self.__axis1, self.__axis2)
C_mandelP_global = Apply_Pmat(P, C_mandel_global)
if self.dim == 2:
if len(shape) == 2:
C_mandelP = C_mandelP_global[idx, :][:, idx]
if len(shape) == 3:
C_mandelP = C_mandelP_global[:, idx, :][:, :, idx]
elif len(shape) == 4:
C_mandelP = C_mandelP_global[:, :, idx, :][:, :, :, idx]
else:
C_mandelP = C_mandelP_global
return C_mandelP
@property
def axis1(self) -> _types.FloatArray:
"""axis1 vector"""
return self.__axis1.copy()
@property
def axis2(self) -> _types.FloatArray:
"""axis2 vector"""
return self.__axis2.copy()
[docs]
def Walpole_Decomposition(self) -> tuple[_types.FloatArray, _types.FloatArray]:
return super().Walpole_Decomposition()