# Library of SPAM functions for dealing with assembly maxtrix of unstructured 3D meshes made of tetrahedra
# Copyright (C) 2020 SPAM Contributors
#
# 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/>.
import numpy
[docs]
def tetCentroid(points):
"""Compute coordinates of the centroid of a tetrahedron
Parameters
----------
points : 4x3 array
Array of the 3D coordinates of the 4 points
Returns
-------
3x1 array
the coordinates of the centroid
"""
centroid = numpy.zeros(3)
for i in range(3):
a = points[0][i]
b = points[1][i]
c = points[2][i]
d = points[3][i]
# mid point of AB (1/2 of A -> B)
mid_ab = a + (b - a) / 2.0
# centroid of ABC (1/3 of AB -> C median)
face_centroid = mid_ab + (c - mid_ab) / 3.0
# centroid of ABCD (1/4 of ABC -> D median)
centroid[i] = face_centroid + (d - face_centroid) / 4.0
return centroid
[docs]
def tetVolume(points):
"""Compute the volume of a tetrahedron
Parameters
----------
points : 4x3 array
Array of the 3D coordinates of the 4 points
Returns
-------
float
the volume of the tetrahedron
"""
# compute the jacobian matrix by padding the points with a line of 1
pad = numpy.array([[1], [1], [1], [1]])
jacobian = numpy.hstack([pad, points])
# the volume is 1/6 of the determinant of the jacobian matrix
return numpy.linalg.det(jacobian) / 6.0
[docs]
def shapeFunctions(points):
"""
This function computes the shape coefficients matrux from the coordinates
of the 4 nodes of a linear tetrahedron (see 4.11 in Zienkiewicz).
.. code-block:: text
coefficients_matrix = [
a1 b1 c1 d1
a2 b2 c2 d2
a3 b3 c3 d3
a4 b4 c4 d4
]
with
N1 = a1 + b1x + c1y + d1z
N2 = a2 + b2x + c2y + d2z
N3 = a3 + b3x + c3y + d3z
N4 = a4 + b4x + c4y + d4z
Parameters
----------
points : 4x3 array
Array of the 3D coordinates of the 4 points
Returns
-------
volume : float
The volume of the tetrahedron
coefficients_matrix : 4x4 array
The coefficients 4 coefficients of the 4 shape functions
Note
-----
Pure python function.
"""
# cast into numpy array and check shape
points = numpy.array(points)
if points.shape != (4, 3):
raise ValueError("Points coordinates in bad format. Can I have a 4x3 please?")
# jacobian matrix
pad = numpy.array([[1], [1], [1], [1]])
jacobian = numpy.hstack([pad, points])
six_v = numpy.linalg.det(jacobian)
volume = six_v / 6.0
coefficients_matrix = numpy.zeros((4, 4))
for l_number in range(4): # loop over the 4 nodes
for c_number in range(4): # loop over the 4 coefficients for 1 node
# create mask for the sub matrix (ie: [1, 2, 3], [0, 2, 3], ...)
lines = [_ for _ in range(4) if _ != l_number]
columns = [_ for _ in range(4) if _ != c_number]
# creates the sub_jacobian (needs two runs for some numpy reason)
sub_jacobian = jacobian[:, columns]
sub_jacobian = sub_jacobian[lines, :]
# compute the determinant and fill coefficients matrix
det = numpy.linalg.det(sub_jacobian)
sign = (-1.0) ** (l_number + c_number)
coefficients_matrix[l_number, c_number] = sign * det / six_v
return volume, coefficients_matrix
[docs]
def elementaryStiffnessMatrix(points, young, poisson):
"""
This function computes elementary stiffness matrix from the coordinates
of the 4 nodes of a linear tetrahedron.
.. code-block:: text
D = [something with E and mu] (6x6)
B = [B1, B2, B3, B4] (4 times 6x3 -> 6x12)
ke = V.BT.D.B (12x12)
Parameters
----------
points : 4x3 array
Array of the 3D coordinates of the 4 points
young: float
Young modulus
poisson: float
Poisson ratio
Returns
-------
stiffness_matrix : 12x12 array
The stiffness matrix of the tetrahedron
Note
-----
Pure python function.
"""
# get volume and shape functions coefficients
volume, N = shapeFunctions(points)
# compute the full B matrix
def Ba(N, a):
return numpy.array(
[
[N[a, 1], 0, 0],
[0, N[a, 2], 0],
[0, 0, N[a, 3]],
[0, N[a, 3], N[a, 2]],
[N[a, 3], 0, N[a, 1]],
[N[a, 2], N[a, 1], 0],
]
)
# initialise B with first Ba
B = Ba(N, 0)
for a in [1, 2, 3]:
B = numpy.hstack([B, Ba(N, a)])
# compute D matrix
D = (1.0 - poisson) * numpy.eye(6)
D[0, [1, 2]] = poisson
D[1, [0, 2]] = poisson
D[2, [0, 1]] = poisson
D[3:, 3:] -= 0.5 * numpy.eye(3)
D *= young / ((1 + poisson) * (1 - 2 * poisson))
# compute stiffness matrix
ke = volume * numpy.matmul(numpy.transpose(B), numpy.matmul(D, B))
return ke
[docs]
def globalStiffnessMatrix(points, connectivity, young, poisson):
"""
This function assembles elementary stiffness matrices to computes an elastic
(3 dof) global stiffness matrix for a 4 noded tetrahedra mesh
Notations
---------
neq: number of global equations
nel: number of elements
npo: number of points
neq = 3 x npo
Parameters
----------
points : npo x 3 array
Array of the 3D coordinates of the all nodes points
connectivity : nel x 4 array
Connectivity matrix
young: float
Young modulus
poisson: float
Poisson ratio
Returns
-------
stiffness_matrix : neq x neq array
The stiffness matrix of the tetrahedron
Note
-----
Pure python function.
"""
# cast to numpy array
points = numpy.array(points)
connectivity = numpy.array(connectivity)
neq = 3 * points.shape[0]
# nel = connectivity.shape[0]
K = numpy.zeros((neq, neq), dtype=float)
for e, element_nodes_number in enumerate(connectivity):
# print(f'Element number {e}: {element_nodes_number}')
# built local stiffness matrix (12x12)
element_points = [points[A] for A in element_nodes_number]
ke = elementaryStiffnessMatrix(element_points, young, poisson)
# loop over stiffness matrix (lines and rows)
for p1 in range(12):
i1 = p1 % 3 # deduce degree of freedom
a1 = (p1 - i1) // 3 # deduce local node number
P1 = 3 * int(connectivity[e, a1]) + i1 # get global equation number
for p2 in range(p1, 12):
i2 = p2 % 3
a2 = (p2 - i2) // 3
P2 = 3 * int(connectivity[e, a2]) + i2
K[P1, P2] += ke[p1, p2]
K = K + K.transpose()
K[numpy.diag_indices(neq)] *= 0.5
return K
if __name__ == "__main__":
import matplotlib.pyplot as plt
import spam.mesh
# points = [
# [0, 0, 0],
# [0, 1, 0],
# [0, 0, 1],
# [1, 0, 0]
# ]
#
# coefficients_matrix = shapeFunctions(points)
# ke = elementaryStiffnessMatrix(points, 1, 0.3)
# print(f'Elementary connectivity matrix: {ke.shape}')
# # print(ke)
lengths = [1, 1, 1]
lc = 0.2
points, connectivity = spam.mesh.createCuboid(
lengths,
lc,
origin=[0.0, 0.0, 0.0],
periodicity=False,
verbosity=1,
gmshFile=None,
vtkFile=None,
binary=False,
skipOutput=False,
)
K = globalStiffnessMatrix(points, connectivity, 1, 0.3)
plt.imshow(numpy.log(K[:100, :100]))
plt.show()
# ID function
def ID(i: int, A: int):
"""
Defined as in Hughes: The Finite Element Method eq. 2.8.3
WARNING: without the condition of dirichlet boundary conditions
Takes
- the degree of freedom: i = 0, 1 or 2
- the node global number: A = 0, 1, 2, ... n_nodes
Returns
- the global equation number: P = 0, 1, 2 ... 3 * n_nodes
"""
P = 3 * A + i
return int(P)
# for A, point in enumerate(points):
# for i in range(3):
# print(f'Node number {A} dof {i}: {ID(i, A)}')
# IEN function
def IEN(a: int, e: int, connectivity):
"""
Defined as in Hughes: The Finite Element Method eq. 2.6.1
Takes
- the local node number: a = 0, 1, 2 or 3
- the element number: e = 0, 1, 2, ... n_elem
Returns
- the node global number: A = 0, 1, 2, ... n_nodes
Uses the connectivity matrix
"""
# connectivity matrix shape: n_elem x 4
A = connectivity[e, a]
return int(A)
# for e, _ in enumerate(connectivity):
# for a in range(4):
# print(f'Element number {e} node number {a}: {IEN(a, e, connectivity)}')
# LM function
def LM(i: int, a: int, e: int, connectivity):
"""
Defined as in Hughes: The Finite Element Method eq. 2.10.1
Takes
- the degree of freedom: i = 0, 1 or 2
- the local node number: a = 0, 1, 2 or 3
- the element number: e = 0, 1, 2, ... n_elem
Returns
- the global equation number: P = 0, 1, 2 ... 3 * n_nodes
Uses the connectivity matrix
"""
P = ID(i, IEN(a, e, connectivity))
return P
# for e, _ in enumerate(connectivity):
# for a in range(4):
# for i in range(3):
# P = 3 * int(connectivity[e, a]) + i
# print(f'Element number {e} node number {a} dof {i}: {LM(i, a, e, connectivity)}')
#
#
# if e == 3:
# break