Examples

exs_spring

Purpose:

Show the basic steps in a finite element calculation.

Description:

The general procedure in linear finite element calculations is carried out for a simple structure. The steps are:

  • Define the model

  • Generate element matrices

  • Assemble element matrices into the global system of equations

  • Solve the global system of equations

  • Evaluate element forces

Consider the system of three linear elastic springs, and the corresponding finite element model. The system of springs is fixed in its ends and loaded by a single load \(F=100 N\) and a stiffness \(k=1500 N/m\)

_images/exs1_1.svg
_images/exs1_2.svg
Example:

The computation is initialized by importing CALFEM and NumPy, then defining the topology matrix edof containing the degrees of freedom for each element:

import numpy as np
import calfem.core as cfc
import calfem.utils as cfu

edof = np.array([
    [1, 2],                 # element 1 between nodes 1 and 2
    [2, 3],                 # element 2 between nodes 2 and 3
    [2, 3]                  # element 3 between nodes 2 and 3
])

The global stiffness matrix K (3×3) of zeros:

K = np.zeros((3, 3))        # empty global stiffness matrix
f = np.zeros((3, 1))        # empty global load vector

And the load vector f (3×1) with the load \(F=100\) at DOF 2:

f[1] = 100.0                # (N), f[1] corresponds to dof 2

Element stiffness matrices are generated by the function cfc.spring1e. The element property ep for the springs contains the spring stiffnesses \(k\) and \(2k\) respectively, where \(k=1500\):

k = 1500.                   # (N/m)
ep1 = 2.*k                  # spring stiffness, element 1
ep2 = k                     # spring stiffness, element 2
ep3 = 2.*k                  # spring stiffness, element 3
Ke1 = cfc.spring1e(ep1)     # element stiffness matrix, element 1
Ke2 = cfc.spring1e(ep2)     # element stiffness matrix, element 2
Ke3 = cfc.spring1e(ep3)     # element stiffness matrix, element 3

cfu.disp_h2("Ke1")
cfu.disp_array(Ke1, tablefmt='plain')
cfu.disp_h2("Ke2")
cfu.disp_array(Ke2, tablefmt='plain')
cfu.disp_h2("Ke3")
cfu.disp_array(Ke3, tablefmt='plain')

The element stiffness matrices are assembled into the global stiffness matrix K according to the topology:

cfc.assem(edof[0, :], K, Ke1)   # assemble element stiffness matrix 1
cfc.assem(edof[1, :], K, Ke2)   # assemble element stiffness matrix 2
cfc.assem(edof[2, :], K, Ke3)   # assemble element stiffness matrix 3

cfu.disp_h2("Stiffness matrix K (N/m):")
cfu.disp_array(K, tablefmt='plain')

The global system of equations is solved considering the boundary conditions. DOFs 1 and 3 are fixed (value 0):

bc_dofs = np.array([1, 3])       # dofs with prescribed displacment (0)
bc_vals = np.array([0.0, 0.0])   # prescribed displacments

a, r = cfc.solveq(K, f, bc_dofs, bc_vals)

cfu.disp_h2("Displacements a (m):")
cfu.disp_array(a, tablefmt="plain")

cfu.disp_h2("Reaction forces r (N):")
cfu.disp_array(r, tablefmt="plain")

Element forces are evaluated from the element displacements. These are obtained from the global displacements a using the function cfc.extract_ed and the spring forces are evaluated using the function cfc.spring1s:

ed1 = cfc.extract_ed(edof[0, :], a) # Nodal displacements, element 1
ed2 = cfc.extract_ed(edof[1, :], a) # Nodal displacements, element 2
ed3 = cfc.extract_ed(edof[2, :], a) # Nodal displacements, element 3

es1 = cfc.spring1s(ep1, ed1)        # Element force, element 1
es2 = cfc.spring1s(ep2, ed2)        # Element force, element 2
es3 = cfc.spring1s(ep3, ed3)        # Element force, element 3

cfu.disp_h2("Element forces (N):")
print("N1 = "+str(es1))
print("N2 = "+str(es2))
print("N3 = "+str(es3))

exs_flw_temp1

Purpose:

Analysis of one-dimensional heat flow.

Description:

Consider a wall built up of concrete and thermal insulation. The outdoor temperature is \(-17°\text{C}\) and the temperature inside is \(20°\text{C}\). At the inside of the thermal insulation there is a heat source yielding \(10\) W/m².

_images/exs2_1.svg
_images/exs2_2.svg

The wall is subdivided into five elements and the one-dimensional spring (analogy) element spring1e is used. Equivalent spring stiffnesses are \(k_i=\lambda A/L\) for thermal conductivity and \(k_i=A/R\) for thermal surface resistance. Corresponding spring stiffnesses per m² of the wall are:

\(k_1 =\)

\(1/0.04\)

\(=\)

\(25.0\)

W/K

\(k_2 =\)

\(1.7/0.070\)

\(=\)

\(24.3\)

W/K

\(k_3 =\)

\(0.040/0.100\)

\(=\)

\(0.4\)

W/K

\(k_4 =\)

\(1.7/0.100\)

\(=\)

\(17.0\)

W/K

\(k_5 =\)

\(1/0.13\)

\(=\)

\(7.7\)

W/K

Example:

The computation is initialized by importing CALFEM and NumPy. A global system matrix K and a heat flow vector f are defined. The heat source inside the wall is considered by setting \(f_4=10\). The element matrices Ke are computed using cfc.spring1e, and the function cfc.assem assembles the global stiffness matrix. The system of equations is solved using cfc.solveq with boundary conditions. The prescribed temperatures are \(T_1=-17°\text{C}\) and \(T_6=20°\text{C}\):

import numpy as np
import calfem.core as cfc
import calfem.utils as cfu

# ----- Topology -------------------------------------------------

edof = np.array([
    [1, 2],
    [2, 3],
    [3, 4],
    [4, 5],
    [5, 6]
])

# ----- Stiffness matrix K and load vector f ---------------------

K = np.array(np.zeros((6, 6)))
f = np.array(np.zeros((6, 1)))
f[3] = 10.0

# ----- Element stiffness and element load matrices  -------------

ep1 = 25
ep2 = 24.3
ep3 = 0.4
ep4 = 17
ep5 = 7.7

Ke1 = cfc.spring1e(ep1)
Ke2 = cfc.spring1e(ep2)
Ke3 = cfc.spring1e(ep3)
Ke4 = cfc.spring1e(ep4)
Ke5 = cfc.spring1e(ep5)

# ----- Assemble Ke into K ---------------------------------------

cfc.assem(edof[0, :], K, Ke1)
cfc.assem(edof[1, :], K, Ke2)
cfc.assem(edof[2, :], K, Ke3)
cfc.assem(edof[3, :], K, Ke4)
cfc.assem(edof[4, :], K, Ke5)

# ----- Solve the system of equations ----------------------------

bc = np.array([1, 6])
bcVal = np.array([-17, 20])
a, r = cfc.solveq(K, f, bc, bcVal)

cfu.disp_h2("Temperatures a:")
cfu.disp_array(a, tablefmt="plain")

cfu.disp_h2("Reaction flows r:")
cfu.disp_array(r, tablefmt="plain")

The temperature values \(a_i\) at the node points are given in the vector a and the boundary heat flows in the vector r.

After solving the system of equations, the heat flow through each element is computed using cfc.extract_ed and cfc.spring1s:

ed1 = cfc.extract_ed(edof[0, :], a)
ed2 = cfc.extract_ed(edof[1, :], a)
ed3 = cfc.extract_ed(edof[2, :], a)
ed4 = cfc.extract_ed(edof[3, :], a)
ed5 = cfc.extract_ed(edof[4, :], a)

q1 = cfc.spring1s(ep1, ed1)
q2 = cfc.spring1s(ep2, ed2)
q3 = cfc.spring1s(ep3, ed3)
q4 = cfc.spring1s(ep4, ed4)
q5 = cfc.spring1s(ep5, ed5)

cfu.disp_h2("Element flows:")

print("q1 = ")
print(q1)
print("q2 = ")
print(q2)
print("q3 = ")
print(q3)
print("q4 = ")
print(q4)
print("q5 = ")
print(q5)

The heat flow through the wall is \(q=14.0\) W/m² in the part of the wall to the left of the heat source (elements 1-3), and \(q=4.0\) W/m² in the part to the right of the heat source (elements 4-5). This demonstrates how the internal heat source affects the heat flow distribution through the wall.

exs_bar2

Purpose:

Analysis of a plane truss.

Description:

Consider a plane truss consisting of three bars with the properties \(E=200\) GPa, \(A_1=6.0 \times 10^{-4}\) m², \(A_2=3.0 \times 10^{-4}\) m² and \(A_3=10.0 \times 10^{-4}\) m², and loaded by a single force \(P=80\) kN. The corresponding finite element model consists of three elements and eight degrees of freedom.

_images/exs3.svg
Example:

The computation is initialized by importing CALFEM and NumPy and defining the element topology matrix containing only the degrees of freedom for each element:

import numpy as np
import calfem.core as cfc
import calfem.utils as cfu
import calfem.vis_mpl as cfv

edof = np.array([
    [1, 2, 5, 6],
    [5, 6, 7, 8],
    [3, 4, 5, 6]
])

The global stiffness matrix K and the load vector f are initialized. The load of 80 kN is applied in the negative y-direction at DOF 6:

K = np.array(np.zeros((8, 8)))
f = np.array(np.zeros((8, 1)))

f[5] = -80e3

The element property vectors ep1, ep2 and ep3 are defined by

E = 2.0e11          # (N/m^2)
A1 = 6.0e-4         # (m^2)
A2 = 3.0e-4         # (m^2)
A3 = 10.0e-4        # (m^2)
ep1 = [E, A1]
ep2 = [E, A2]
ep3 = [E, A3]

and the element coordinate vectors ex1, ex2, ex3, ey1, ey2 and ey3 by

ex1 = np.array([0.0, 1.6])
ex2 = np.array([1.6, 1.6])
ex3 = np.array([0.0, 1.6])

ey1 = np.array([0.0, 0.0])
ey2 = np.array([0.0, 1.2])
ey3 = np.array([1.2, 0.0])

The element stiffness matrices Ke1, Ke2 and Ke3 are computed using bar2e:

Ke1 = cfc.bar2e(ex1, ey1, ep1)
Ke2 = cfc.bar2e(ex2, ey2, ep2)
Ke3 = cfc.bar2e(ex3, ey3, ep3)

Ke1 = cfc.bar2e(ex1, ey1, ep1)
Ke2 = cfc.bar2e(ex2, ey2, ep2)
Ke3 = cfc.bar2e(ex3, ey3, ep3)

cfu.disp_h2("Ke1")
cfu.disp_array(Ke1, tablefmt='plain')
cfu.disp_h2("Ke2")
cfu.disp_array(Ke2, tablefmt='plain')
cfu.disp_h2("Ke3")
cfu.disp_array(Ke3, tablefmt='plain')

Based on the topology information, the global stiffness matrix can be generated by assembling the element stiffness matrices.

K = cfc.assem(edof[0, :], K, Ke1)
K = cfc.assem(edof[1, :], K, Ke2)
K = cfc.assem(edof[2, :], K, Ke3)

cfu.disp_h2("Stiffness matrix K (N/m):")
cfu.disp_array(K, tablefmt='plain')

The system of equations \(\boldsymbol{Ka}=\boldsymbol{f}\) is solved by specifying the boundary conditions and using solveq():

bc_dofs = np.array([1, 2, 3, 4, 7, 8])
bc_vals = np.array([0.0, 0.0, 0.0, 0.0, 0.0, 0.0])
a, r = cfc.solveq(K, f, bc_dofs, bc_vals)

cfu.disp_h2("Displacements a (m):")
cfu.disp_array(a)

cfu.disp_h2("Reaction forces r (N):")
cfu.disp_array(r)

The vertical displacement at the point of loading is 1.15 mm. The element forces are calculated by extracting element displacements and computing stresses:

ed1 = cfc.extract_ed(edof[0, :], a)
ed2 = cfc.extract_ed(edof[1, :], a)
ed3 = cfc.extract_ed(edof[2, :], a)

es1 = cfc.bar2s(ex1, ey1, ep1, ed1)
es2 = cfc.bar2s(ex2, ey2, ep2, ed2)
es3 = cfc.bar2s(ex3, ey3, ep3, ed3)

cfu.disp_h2("Element normal forces (N):")

cfu.disp("N1")
cfu.disp_array(es1)
cfu.disp("N2")
cfu.disp_array(es2)
cfu.disp("N3")
cfu.disp_array(es3)

The normal forces are \(N_1=-29.84\) kN (compression), \(N_2=57.62\) kN (tension) and \(N_3=37.31\) kN (tension).

Visualization of the results can be performed using CALFEM’s visualization functions:

plotpar1 = [2, 1, 0]
sfac = cfv.scalfact2(ex3, ey3, ed1, 0.1)
print("sfac =", sfac)

cfv.figure(1)
cfv.eldraw2(ex1, ey1, plotpar1)
cfv.eldraw2(ex2, ey2, plotpar1)
cfv.eldraw2(ex3, ey3, plotpar1)

plotpar2 = [1, 2, 1]
cfv.eldisp2(ex1, ey1, ed1, plotpar2, sfac)
cfv.eldisp2(ex2, ey2, ed2, plotpar2, sfac)
cfv.eldisp2(ex3, ey3, ed3, plotpar2, sfac)
cfv.axis([-0.4, 2.0, -0.4, 1.4])
plotpar3 = 2
cfv.scalgraph2(sfac, [1e-3, 0, -0.3], plotpar3)
cfv.title("Displacements")

# ----- Draw normal force diagram --------------------------------

plotpar = [2, 1]
sfac = cfv.scalfact2(ex1, ey1, es2[:, 0], 0.1)
cfv.figure(2)
cfv.secforce2(ex1, ey1, es1[:, 0], plotpar, sfac)
cfv.secforce2(ex2, ey2, es2[:, 0], plotpar, sfac)
cfv.secforce2(ex3, ey3, es3[:, 0], plotpar, sfac)
cfv.axis([-0.4, 2.0, -0.4, 1.4])
cfv.scalgraph2(sfac, [5e4, 0, -0.3], plotpar3)
cfv.title("Normal force")

cfv.show_and_wait()
Figures:
_images/exs3_1.svg
_images/exs3_2.svg

exs_bar2_l

Purpose:

Analysis of a plane truss.

Description:

Consider a plane truss, loaded by a single force \(P=0.5\) MN.

_images/exs4_1.svg

The corresponding finite element model consists of ten elements and twelve degrees of freedom.

_images/exs4_2.svg

Material properties:

  • Cross-sectional area: \(A=25.0 \times 10^{-4}\)

  • Young’s modulus: \(E=2.10 \times 10^{5}\) MPa

Example:

The computation is initialized by importing CALFEM and NumPy. The element topology matrix contains only the degrees of freedom for each element:

import numpy as np
import calfem.core as cfc
import calfem.utils as cfu

edof = np.array([
    [1, 2, 5, 6],
    [3, 4, 7, 8],
    [5, 6, 9, 10],
    [7, 8, 11, 12],
    [7, 8, 5, 6],
    [11, 12, 9, 10],
    [3, 4, 5, 6],
    [7, 8, 9, 10],
    [1, 2, 7, 8],
    [5, 6, 11, 12],
])

The global stiffness matrix K and load vector f are initialized. The load \(P=0.5\) MN is divided into x and y components:

K = np.zeros([12, 12])
f = np.zeros([12, 1])
f[10] = 0.5e6 * np.sin(np.pi / 6)
f[11] = -0.5e6 * np.cos(np.pi / 6)

The material and geometric properties are defined, along with element coordinate matrices:

A = 25.0e-4
E = 2.1e11
ep = [E, A]

ex = np.array([
    [0.0, 2.0],
    [0.0, 2.0],
    [2.0, 4.0],
    [2.0, 4.0],
    [2.0, 2.0],
    [4.0, 4.0],
    [0.0, 2.0],
    [2.0, 4.0],
    [0.0, 2.0],
    [2.0, 4.0],
])

ey = np.array([
    [2.0, 2.0],
    [0.0, 0.0],
    [2.0, 2.0],
    [0.0, 0.0],
    [0.0, 2.0],
    [0.0, 2.0],
    [0.0, 2.0],
    [0.0, 2.0],
    [2.0, 0.0],
    [2.0, 0.0],
])

The element stiffness matrices are computed and assembled in a loop:

for elx, ely, eltopo in zip(ex, ey, edof):
    Ke = cfc.bar2e(elx, ely, ep)
    cfc.assem(eltopo, K, Ke)

The system of equations is solved by specifying boundary conditions and using solveq():

bc_dofs = np.array([1, 2, 3, 4])
bc_vals = np.array([0.0, 0.0, 0.0, 0.0])
a, r = cfc.solveq(K, f, bc_dofs, bc_vals)

cfu.disp_h2("Displacements a:")
cfu.disp_array(a, tablefmt='plain')

cfu.disp_h2("Reaction forces r:")
cfu.disp_array(r, tablefmt='plain')

The displacement at the point of loading is \(-1.7 \times 10^{-3}\) m in the x-direction and \(-11.3 \times 10^{-3}\) m in the y-direction. At the upper support the horizontal force is \(-0.866\) MN and the vertical \(0.240\) MN. At the lower support the forces are \(0.616\) MN and \(0.193\) MN, respectively.

Normal forces are evaluated from element displacements. These are obtained from the global displacements using extract_ed() and the forces are calculated using bar2s():

ed = cfc.extract_ed(edof, a)
N = np.zeros([edof.shape[0]])

cfu.disp_h2("Element forces:")

i = 0
for elx, ely, eld in zip(ex, ey, ed):
    es = cfc.bar2s(elx, ely, ep, eld)
    N[i] = es[0][0]
    print("N%d = %g" % (i + 1, N[i]))
    i += 1

The largest normal force \(N=0.626\) MN is obtained in element 1 and is equivalent to a normal stress \(\sigma=250\) MPa.

To reduce the quantity of input data, the element coordinate matrices ex and ey can alternatively be created from a global coordinate matrix coord and a global topology matrix dof using coord_extract():

coord = np.array([
    [0, 2],
    [0, 0],
    [2, 2],
    [2, 0],
    [4, 2],
    [4, 0]
])

dof = np.array([
    [1, 2],
    [3, 4],
    [5, 6],
    [7, 8],
    [9, 10],
    [11, 12]
])

ex, ey = cfc.coord_extract(edof, coord, dof, 2)

exs_beam1

Purpose:

Analysis of a simply supported beam.

Description:

Consider a beam with the length \(9.0\) m. The beam is simply supported and loaded by a point load \(P=10000\) N applied at a point \(3.0\) m from the left support. The corresponding computational model has six degrees of freedom and consists of two beam elements with four degrees of freedom. The beam has Young’s modulus \(E=210\) GPa and moment of inertia \(I=2510 \times 10^{-8}\) m⁴.

_images/exs5_1.svg
_images/exs5_2.svg
Example:

The computation is initialized by importing CALFEM and NumPy. The element topology matrix contains only the degrees of freedom for each element:

import numpy as np
import matplotlib.pyplot as plt
import calfem.core as cfc
import calfem.utils as cfu
import calfem.vis_mpl as cfv

edof = np.array([
    [1, 2, 3, 4],
    [3, 4, 5, 6]
])

The global stiffness matrix K and load vector f are initialized. The point load P = 10000 N is applied at DOF 3:

K = np.array(np.zeros((6, 6)))
f = np.array(np.zeros((6, 1)))
f[2] = -10e3

The material and geometric properties are defined, along with element coordinates and stiffness matrices:

E = 210e9
I = 2510e-8

ep = np.array([E, I])
ex1 = np.array([0, 3])
ex2 = np.array([3, 9])
eq1 = np.array([0])
eq2 = np.array([0])

Ke1 = cfc.beam1e(ex1, ep)
Ke2 = cfc.beam1e(ex2, ep)

cfu.disp_h2("Ke1")
cfu.disp_array(Ke1, tablefmt='plain')
cfu.disp_h2("Ke2")
cfu.disp_array(Ke2, tablefmt='plain')

Based on the topology information, the global stiffness matrix can be generated by assembling the element stiffness matrices.

cfc.assem(edof[0, :], K, Ke1)
cfc.assem(edof[1, :], K, Ke2)

The system of equations is solved by defining boundary conditions and using solveq():

bc_dofs = np.array([1, 5])
bc_vals = np.array([0.0, 0.0])
a, r = cfc.solveq(K, f, bc_dofs, bc_vals)

cfu.disp_array(a, ["a"])
cfu.disp_array(r, ["r"])

The section forces and element displacements are calculated from global displacements:

ed = cfc.extract_ed(edof, a)

es1, ed1, ec1 = cfc.beam1s(ex1, ep, ed[0, :], eq1, nep=4)
es2, ed2, ec2 = cfc.beam1s(ex2, ep, ed[1, :], eq2, nep=7)

cfu.disp_h2("es1")
cfu.disp_array(es1, ["V1", "M1"])
cfu.disp_h2("ed1")
cfu.disp_array(ed1, ["v1"])
cfu.disp_h2("ec1")
cfu.disp_array(ec1, ["x1"])
cfu.disp_h2("es2")
cfu.disp_array(es2, ["V2", "M2"])
cfu.disp_h2("ed2")
cfu.disp_array(ed2, ["v2"])
cfu.disp_h2("ec2")
cfu.disp_array(ec2, ["x2"])

Results:

The solution shows the expected behavior for a simply supported beam with a point load:

  • Maximum deflection: 22.8 mm at the loading point (DOF 3)

  • Support reactions: 6667 N at left support, 3333 N at right support

  • Maximum shear force: 6667 N (in element 1)

  • Maximum moment: 20000 Nm (at the loading point)

Visualization of the results using matplotlib:

cfv.figure(1)
plt.plot([0, 9], [0, 0], color=(0.8, 0.8, 0.8))
plt.plot(
    np.concatenate(([0], ec1[:, 0], 3 + ec2[:, 0], [9]), 0),
    np.concatenate(([0], ed1[:, 0], ed2[:, 0], [0]), 0),
    color=(0.0, 0.0, 0.0),
)
cfv.title("displacements")

# ----- Draw shear force diagram----------------------------------

cfv.figure(2)
plt.plot([0, 9], [0, 0], color=(0.8, 0.8, 0.8))
plt.plot(
    np.concatenate(([0], ec1[:, 0], 3 + ec2[:, 0], [9]), 0),
    -np.concatenate(([0], es1[:, 0], es2[:, 0], [0]), 0) / 1000,
    color=(0.0, 0.0, 0.0),
)
cfv.title("shear force")

# ----- Draw moment diagram----------------------------------

cfv.figure(3)
plt.plot([0, 9], [0, 0], color=(0.8, 0.8, 0.8))
plt.plot(
    np.concatenate(([0], ec1[:, 0], 3 + ec2[:, 0], [9]), 0),
    -np.concatenate(([0], es1[:, 1], es2[:, 1], [0]), 0) / 1000,
    color=(0.0, 0.0, 0.0),
)
cfv.title("bending moment")

cfv.show_and_wait()
_images/exs5_3.svg
_images/exs5_4.svg
_images/exs5_5.svg

exs_beam2

Purpose:

Analysis of a plane frame.

Description:

A frame consists of one horizontal and two vertical beams according to the figure.

_images/exs6_1.svg

Material and geometric properties:

\(E\)

\(=\)

\(200\) GPa

\(A_1\)

\(=\)

\(2.0 \times 10^{-3}\)

\(I_1\)

\(=\)

\(1.6 \times 10^{-5}\) m⁴

\(A_2\)

\(=\)

\(6.0 \times 10^{-3}\)

\(I_2\)

\(=\)

\(5.4 \times 10^{-5}\) m⁴

\(P\)

\(=\)

\(2.0\) kN

\(q_0\)

\(=\)

\(10.0\) kN/m

The corresponding finite element model consists of three beam elements and twelve degrees of freedom.

_images/exs6_2.svg
Example:

The computation is initialized by importing CALFEM and NumPy. The element topology matrix contains only the degrees of freedom for each element:

import numpy as np
import calfem.core as cfc
import calfem.utils as cfu
import calfem.vis_mpl as cfv

edof = np.array([
    [4, 5, 6, 1, 2, 3],
    [7, 8, 9, 10, 11, 12],
    [4, 5, 6, 7, 8, 9]
])

The global stiffness matrix K and load vector f are initialized. The point load P = 2000 N is applied at DOF 4:

K = np.array(np.zeros((12, 12)))
f = np.array(np.zeros((12, 1)))
f[3] = 2.0e3

The material and geometric properties are defined for each element type:

E = 200.0e9
A1 = 2.0e-3
A2 = 6.0e-3
I1 = 1.6e-5
I2 = 5.4e-5

ep1 = [E, A1, I1]
ep3 = [E, A2, I2]

ex1 = np.array([0, 0])
ex2 = np.array([6, 6])
ex3 = np.array([0, 6])

ey1 = np.array([4, 0])
ey2 = np.array([4, 0])
ey3 = np.array([4, 4])

eq1 = np.array([0, 0])
eq2 = np.array([0, 0])
eq3 = np.array([0, -10e3])

The element stiffness matrices and load vectors are computed and assembled:

Ke1 = cfc.beam2e(ex1, ey1, ep1)
Ke2 = cfc.beam2e(ex2, ey2, ep1)
Ke3, fe3 = cfc.beam2e(ex3, ey3, ep3, eq3)

K = cfc.assem(edof[0, :], K, Ke1)
K = cfc.assem(edof[1, :], K, Ke2)
K, f = cfc.assem(edof[2, :], K, Ke3, f, fe3)

The system of equations is solved by specifying boundary conditions and using solveq():

bc_dofs = np.array([1, 2, 3, 10, 11])
bc_vals = np.array([0.0, 0.0, 0.0, 0.0, 0.0])

a, r = cfc.solveq(K, f, bc_dofs, bc_vals)

cfu.disp_array(a, ["a"])
cfu.disp_array(r, ["r"])

The element displacements are extracted and section forces are computed along each element:

ed = cfc.extract_ed(edof, a)

es1, edi1, ec1 = cfc.beam2s(ex1, ey1, ep1, ed[0, :], eq1, nep=21)
es2, edi2, ec2 = cfc.beam2s(ex2, ey2, ep1, ed[1, :], eq2, nep=21)
es3, edi3, ec3 = cfc.beam2s(ex3, ey3, ep3, ed[2, :], eq3, nep=21)

cfu.disp_h2("es1")
cfu.disp_array(es1, ["N", "Vy", "Mz"])
cfu.disp_h2("edi1")
cfu.disp_array(edi1, ["u1", "v1"])
cfu.disp_h2("es2")
cfu.disp_array(es2, ["N", "Vy", "Mz"])
cfu.disp_h2("edi2")
cfu.disp_array(edi2, ["u1", "v1"])
cfu.disp_h2("es3")
cfu.disp_array(es3, ["N", "Vy", "Mz"])
cfu.disp_h2("edi3")
cfu.disp_array(edi3, ["u1", "v1"])

Visualization of the frame analysis results using CALFEM’s visualization functions:

plotpar = [2, 1, 0]
sfac = cfv.scalfact2(ex3, ey3, edi3, 0.1)
print("sfac=")
print(sfac)

cfv.figure(1)
cfv.eldraw2(ex1, ey1, plotpar)
cfv.eldraw2(ex2, ey2, plotpar)
cfv.eldraw2(ex3, ey3, plotpar)

plotpar = [1, 2, 1]
cfv.dispbeam2(ex1, ey1, edi1, plotpar, sfac)
cfv.dispbeam2(ex2, ey2, edi2, plotpar, sfac)
cfv.dispbeam2(ex3, ey3, edi3, plotpar, sfac)
cfv.axis([-1.5, 7.5, -0.5, 5.5])
plotpar1 = 2
cfv.scalgraph2(sfac, [1e-2, 0.5, 0], plotpar1)
cfv.title("Displacements")

# ----- Draw normal force diagram --------------------------------

plotpar = [2, 1]
sfac = cfv.scalfact2(ex1, ey1, es1[:, 0], 0.2)
cfv.figure(2)
cfv.secforce2(ex1, ey1, es1[:, 0], plotpar, sfac)
cfv.secforce2(ex2, ey2, es2[:, 0], plotpar, sfac)
cfv.secforce2(ex3, ey3, es3[:, 0], plotpar, sfac)
cfv.axis([-1.5, 7.5, -0.5, 5.5])
cfv.scalgraph2(sfac, [3e4, 1.5, 0], plotpar1)
cfv.title("Normal force")

# ----- Draw shear force diagram ---------------------------------

plotpar = [2, 1]
sfac = cfv.scalfact2(ex3, ey3, es3[:, 1], 0.2)
cfv.figure(3)
cfv.secforce2(ex1, ey1, es1[:, 1], plotpar, sfac)
cfv.secforce2(ex2, ey2, es2[:, 1], plotpar, sfac)
cfv.secforce2(ex3, ey3, es3[:, 1], plotpar, sfac)
cfv.axis([-1.5, 7.5, -0.5, 5.5])
cfv.scalgraph2(sfac, [3e4, 0.5, 0], plotpar1)
cfv.title("Shear force")

# ----- Draw moment diagram --------------------------------------

plotpar = [2, 1]
sfac = cfv.scalfact2(ex3, ey3, es3[:, 2], 0.2)
print("sfac=")
print(sfac)

cfv.figure(4)
cfv.secforce2(ex1, ey1, es1[:, 2], plotpar, sfac)
cfv.secforce2(ex2, ey2, es2[:, 2], plotpar, sfac)
cfv.secforce2(ex3, ey3, es3[:, 2], plotpar, sfac)
cfv.axis([-1.5, 7.5, -0.5, 5.5])
cfv.scalgraph2(sfac, [3e4, 0.5, 0], plotpar1)
cfv.title("Moment")

cfv.show_and_wait()
_images/exs6_3.svg
_images/exs6_4.svg
_images/exs6_5.svg
_images/exs6_6.svg

exs_beambar2

Purpose:

Analysis of a combined beam and bar structure.

Description:

Consider a structure consisting of a beam with \(A_1=4.0 \times 10^{-3}\) m² and \(I_1=5.4 \times 10^{-5}\) m⁴ supported by two bars with \(A_2=1.0 \times 10^{-3}\) m². The beam as well as the bars have \(E=200\) GPa. The structure is loaded by a distributed load \(q=10\) kN/m. The corresponding finite element model consists of three beam elements and two bar elements and has 14 degrees of freedom.

_images/exs7_1.svg
_images/exs7_2.svg
Example:

The computation is initialized by importing CALFEM and NumPy. The topology matrices are defined separately for beam and bar elements, containing only the degrees of freedom:

import numpy as np
import calfem.core as cfc
import calfem.utils as cfu
import calfem.vis_mpl as cfv
edof1 = np.array([
    [1,  2,  3,  4,  5,  6],
    [4,  5,  6,  7,  8,  9],
    [7,  8,  9, 10, 11, 12]
])

edof2 = np.array([
    [13, 14,  4,  5],
    [13, 14,  7,  8]
])

K = np.array(np.zeros((14, 14)))
f = np.array(np.zeros((14, 1)))

The material and geometric properties are defined for beam and bar elements:

E = 200.e9
A1 = 4.e-3
I1 = 5.4e-5
A2 = 1.e-3

ep1 = [E, A1, I1]
ep4 = [E, A2]

eq1 = [0, 0]
eq2 = [0, -10e+3]

ex1 = np.array([0, 2])
ex2 = np.array([2, 4])
ex3 = np.array([4, 6])
ex4 = np.array([0, 2])
ex5 = np.array([0, 4])
ey1 = np.array([2, 2])
ey2 = np.array([2, 2])
ey3 = np.array([2, 2])
ey4 = np.array([0, 2])
ey5 = np.array([0, 2])

The element stiffness matrices are computed using beam2e() for beam elements and bar2e() for bar elements. Element load vectors from distributed loads are also computed:

Ke1 = cfc.beam2e(ex1, ey1, ep1)
Ke2, fe2 = cfc.beam2e(ex2, ey2, ep1, eq2)
Ke3, fe3 = cfc.beam2e(ex3, ey3, ep1, eq2)

Ke4 = cfc.bar2e(ex4, ey4, ep4)
Ke5 = cfc.bar2e(ex5, ey5, ep4)

The global stiffness matrix and load vector are assembled using assem():

K = cfc.assem(edof1[0, :], K, Ke1)
K, f = cfc.assem(edof1[1, :], K, Ke2, f, fe2)
K, f = cfc.assem(edof1[2, :], K, Ke3, f, fe3)
K = cfc.assem(edof2[0, :], K, Ke4)
K = cfc.assem(edof2[1, :], K, Ke5)

The system of equations is solved by specifying boundary conditions and using solveq(). The vertical displacement at the end of the beam is 13.0 mm:

bc_dofs = np.array([1, 2, 3, 13, 14])
bc_vals = np.array([0.0, 0.0, 0.0, 0.0, 0.0])

a, r = cfc.solveq(K, f, bc_dofs, bc_vals)
cfu.disp_h2("Displacements a:")
cfu.disp_array(a)

cfu.disp_h2("Reaction forces r:")
cfu.disp_array(r)

Maximum vertical displacement: 0.009 m = 9.5 mm

The section forces are calculated using beam2s() and bar2s() from element displacements. This yields normal forces of -35.4 kN and -152.5 kN in the bars and maximum moment of 20.0 kNm in the beam:

ed1 = cfc.extract_ed(edof1, a)
ed2 = cfc.extract_ed(edof2, a)

es1, _, _ = cfc.beam2s(ex1, ey1, ep1, ed1[0, :], eq1, nep=11)
es2, _, _ = cfc.beam2s(ex2, ey2, ep1, ed1[1, :], eq2, nep=11)
es3, _, _ = cfc.beam2s(ex3, ey3, ep1, ed1[2, :], eq2, nep=11)
es4 = cfc.bar2s(ex4, ey4, ep4, ed2[0, :])
es5 = cfc.bar2s(ex5, ey5, ep4, ed2[1, :])

cfu.disp_h2("es1 = ")
cfu.disp_array(es1, headers=["N", "Q", "M"])
cfu.disp_h2("es2 = ")
cfu.disp_array(es2, headers=["N", "Q", "M"])
cfu.disp_h2("es3 = ")
cfu.disp_array(es3, headers=["N", "Q", "M"])
cfu.disp_h2("es4 = ")
cfu.disp_array(es4, headers=["N"])
cfu.disp_h2("es5 = ")
cfu.disp_array(es5, headers=["N"])

exs_flw_diff2

Purpose:

Show how to solve a two dimensional diffusion problem.

Description:

Consider a filter paper of square shape. Three sides are in contact with pure water and the fourth side is in contact with a solution of concentration \(c=1.0 \cdot 10^{-3}\) kg/m³. The length of each side is 0.100 m. Using symmetry, only half of the paper has to be analyzed. The paper and the corresponding finite element mesh are shown. The following boundary conditions are applied:

_images/exs8f1.svg
Example:

The computation is initialized by importing CALFEM and NumPy. The element topology matrix contains only the degrees of freedom for each quadrilateral element:

import numpy as np
import calfem.vis_mpl as cfv
import calfem.core as cfc
import calfem.utils as cfu

edof = np.array([
   [1, 2, 5, 4],
   [2, 3, 6, 5],
   [4, 5, 8, 7],
   [5, 6, 9, 8],
   [7, 8, 11, 10],
   [8, 9, 12, 11],
   [10, 11, 14, 13],
   [11, 12, 15, 14],
])

coord = np.array([
   [0, 0],
   [0.025, 0],
   [0.05, 0],
   [0, 0.025],
   [0.025, 0.025],
   [0.05, 0.025],
   [0, 0.05],
   [0.025, 0.05],
   [0.05, 0.05],
   [0, 0.075],
   [0.025, 0.075],
   [0.05, 0.075],
   [0, 0.1],
   [0.025, 0.1],
   [0.05, 0.1],
])

dofs = np.arange(1, coord.shape[0] + 1).reshape(coord.shape[0], 1)

ep = [1]

D = np.array([
   [1, 0],
   [0, 1]
])

ex, ey = cfc.coordxtr(edof, coord, dofs)

The global system matrices are initialized:

K = np.zeros((15, 15))  # Global conductivity matrix
f = np.zeros((15, 1))   # Global source vector (no sources in this problem)

The global conductivity matrix is assembled by adding the element matrix computed with flw2qe() to each element location:

for elx, ely, etopo in zip(ex, ey, edof):
   Ke = cfc.flw2qe(elx, ely, ep, D)
   K = cfc.assem(etopo, K, Ke)

The boundary conditions are applied and the system is solved. The boundary condition at DOF 13 is set to 0.5×10⁻³ as an average of neighboring boundary concentrations:

bc_dofs = np.array([1, 2, 3, 4, 7, 10, 13, 14, 15])
bc_vals = np.array([0, 0, 0, 0, 0, 0, 0.5e-3, 1e-3, 1e-3])

Solve for concentrations and boundary fluxes

a, r = cfc.solveq(K, f, bc_dofs, bc_vals)

cfu.disp_h2("Concentration at nodes [kg/m^3]:")
cfu.disp_array(a, ["a"])

cfu.disp_h2("Boundary fluxes at nodes [kg/m^2/s)]:")
cfu.disp_array(r, ["r"])

The element flux vectors are calculated from element concentrations using flw2qs():

ed = cfc.extract_eldisp(edof, a)

Compute element flux vectors for all elements.

es = np.zeros((8, 2))
el_idx = 0

for elx, ely, eld in zip(ex, ey, ed):
   es_el, t = cfc.flw2qs(elx, ely, ep, D, eld)
   es[el_idx] = es_el

   el_idx += 1

   cfu.disp_h2(f"Element flux vectors [kg/m^2/s]:")
   cfu.disp_array(es, headers=["qx", "qy"])

   cfu.disp_h2("Concentration field [×10⁻³ kg/m³]:")
   cfu.disp("Pure water boundaries (DOFs 1-4,7,10): 0.000")
   cfu.disp(f"Internal concentrations:")

   a_dofs = [5, 6, 8, 9, 11, 12]
   a_internal = a[a_dofs - np.ones(len(a_dofs), dtype=int)]
   a_table = np.hstack((np.array(a_dofs, dtype=int).reshape(-1, 1), a_internal.reshape(-1, 1)))

   cfu.disp_array(a_table, headers=["DOF", "Concentration"])

   cfu.disp(f"Solution boundaries (DOFs 14,15): 1.000")
import calfem.vis_mpl as cfv
import calfem.core as cfc
import calfem.utils as cfu

Visualization can be created using CALFEM’s visualization functions:

>>> cfv.eldraw2(ex, ey, [1, 2, 1], range(1, ex.shape[0] + 1))
>>> cfv.eliso2_mpl(ex, ey, ed)
>>> cfv.show_and_wait()
_images/exs8f2.svg
_images/exs8f3.svg

Note

Two comments concerning the contour lines:

In the upper left corner, the contour lines should physically have met at the corner point. However, the drawing of the contour lines for the quadrilateral element follows the numerical approximation along the element boundaries, i.e. a linear variation. A finer element mesh will bring the contour lines closer to the corner point.

Along the symmetry line, the contour lines should physically be perpendicular to the boundary. This will also be improved with a finer element mesh.

exd_beam2_m

Purpose:

Set up the finite element model and perform eigenvalue analysis for a simple frame structure.

Description:

Consider the two dimensional frame shown below. A vertical beam is fixed at its lower end, and connected to a horizontal beam at its upper end. The horizontal beam is simply supported at the right end. The length of the vertical beam is 3 m and of the horizontal beam 2 m.

The following data apply to the beams:

vertical beam

horizontal beam

Young’s modulus (N/m²)

\(3 \times 10^{10}\)

\(3 \times 10^{10}\)

Cross section area (m²)

\(0.1030 \times 10^{-2}\)

\(0.0764 \times 10^{-2}\)

Moment of inertia (m⁴)

\(0.171 \times 10^{-5}\)

\(0.0801 \times 10^{-5}\)

Density (kg/m³)

2500

2500

_images/exd1fbig.svg
  1. Frame structure b) Element and DOF numbering

The structure is divided into 4 elements. The numbering of elements and degrees-of-freedom are apparent from the figure. The following .m-file defines the finite element model:

Example:

The computation is initialized by importing CALFEM and NumPy. Material data and topology are defined:

>>> import numpy as np
>>> import calfem.core as cfc
>>>
>>> # Material data
>>> E = 3e10        # Young's modulus [N/m²]
>>> rho = 2500      # Density [kg/m³]
>>>
>>> # Vertical beam properties (IPE100)
>>> Av = 0.1030e-2   # Cross-sectional area [m²]
>>> Iv = 0.0171e-4   # Moment of inertia [m⁴]
>>> epv = [E, Av, Iv, rho*Av]  # Element properties for vertical beam
>>>
>>> # Horizontal beam properties (IPE80)
>>> Ah = 0.0764e-2   # Cross-sectional area [m²]
>>> Ih = 0.00801e-4  # Moment of inertia [m⁴]
>>> eph = [E, Ah, Ih, rho*Ah]  # Element properties for horizontal beam
>>>
>>> print("Material properties defined successfully")
Material properties defined successfully
>>> # Element topology matrix (DOFs only, no element numbers)
>>> Edof = np.array([
...     [1, 2, 3, 4, 5, 6],      # Element 1: vertical beam lower
...     [4, 5, 6, 7, 8, 9],      # Element 2: vertical beam upper
...     [7, 8, 9, 10, 11, 12],   # Element 3: horizontal beam left
...     [10, 11, 12, 13, 14, 15] # Element 4: horizontal beam right
... ])
>>>
>>> # Global coordinate matrix [m]
>>> Coord = np.array([
...     [0, 0],     # Node 1: base of vertical beam
...     [0, 1.5],   # Node 2: mid vertical beam
...     [0, 3],     # Node 3: top of vertical beam / left end of horizontal
...     [1, 3],     # Node 4: mid horizontal beam
...     [2, 3]      # Node 5: right end of horizontal beam
... ])
>>>
>>> # Degrees of freedom numbering
>>> Dof = np.array([
...     [1, 2, 3],     # Node 1 DOFs
...     [4, 5, 6],     # Node 2 DOFs
...     [7, 8, 9],     # Node 3 DOFs
...     [10, 11, 12],  # Node 4 DOFs
...     [13, 14, 15]   # Node 5 DOFs
... ])

Element matrices are generated and assembled into global stiffness and mass matrices:

>>> # Initialize global matrices
>>> K = np.zeros((15, 15))  # Global stiffness matrix
>>> M = np.zeros((15, 15))  # Global mass matrix
>>>
>>> # Extract element coordinates
>>> Ex, Ey = cfc.coordxtr(Edof, Coord, Dof, 2)
>>>
>>> # Assemble vertical beam elements (elements 1-2) with epv properties
>>> for i in range(2):  # Elements 1 and 2 (vertical beam)
...     k, m, c = cfc.beam2de(Ex[i], Ey[i], epv)
...     K = cfc.assem(Edof[i], K, k)
...     M = cfc.assem(Edof[i], M, m)
>>>
>>> # Assemble horizontal beam elements (elements 3-4) with eph properties
>>> for i in range(2, 4):  # Elements 3 and 4 (horizontal beam)
...     k, m, c = cfc.beam2de(Ex[i], Ey[i], eph)
...     K = cfc.assem(Edof[i], K, k)
...     M = cfc.assem(Edof[i], M, m)
>>>
>>> print("Global stiffness and mass matrices assembled successfully")
Global stiffness and mass matrices assembled successfully

The finite element mesh can be plotted using CALFEM visualization functions:

>>> import calfem.vis_mpl as cfv
>>> import matplotlib.pyplot as plt
>>>
>>> # Plot finite element mesh
>>> cfv.figure(figsize=(10, 8))
>>> cfv.eldraw2(Ex, Ey, [1, 2, 2], Edof[:, 0])  # Draw elements with numbering
>>> plt.grid(True, alpha=0.3)
>>> plt.title('2D Frame Structure')
>>> plt.xlabel('x [m]')
>>> plt.ylabel('y [m]')
>>> plt.axis('equal')
>>> plt.show()
_images/exd1f2.svg

Eigenvalue analysis is performed to determine natural frequencies and mode shapes:

>>> # Boundary conditions (constrained DOFs)
>>> b = np.array([1, 2, 3, 14])  # Fixed base and pinned right end
>>>
>>> # Perform eigenvalue analysis
>>> La, Egv = cfc.eigen(K, M, b)
>>>
>>> # Calculate natural frequencies in Hz
>>> Freq = np.sqrt(La) / (2 * np.pi)
>>>
>>> print("Natural frequencies [Hz]:")
>>> for i, f in enumerate(Freq):
...     print(f"Mode {i+1}: {f:.4f} Hz")
Natural frequencies [Hz]:
Mode 1: 6.9826 Hz
Mode 2: 43.0756 Hz
Mode 3: 66.5772 Hz
Mode 4: 162.7453 Hz
Mode 5: 230.2709 Hz
Mode 6: 295.6136 Hz
Mode 7: 426.2271 Hz
Mode 8: 697.7628 Hz
Mode 9: 877.2765 Hz
Mode 10: 955.9809 Hz
Mode 11: 1751.3000 Hz

Note that the boundary condition array b lists only the constrained degrees-of-freedom. The eigenvalues are stored in La, eigenvectors in Egv, and frequencies in Freq:

\[\begin{split}\text{Freq} = \begin{bmatrix} 6.9826 \\ 43.0756 \\ 66.5772 \\ 162.7453 \\ 230.2709 \\ 295.6136 \\ 426.2271 \\ 697.7628 \\ 877.2765 \\ 955.9809 \\ 1751.3 \end{bmatrix}^T\end{split}\]

Individual eigenvectors (mode shapes) can be plotted:

>>> # Plot the first eigenmode
>>> cfv.figure(1, figsize=(10, 8))
>>> plt.grid(True, alpha=0.3)
>>> plt.title('The first eigenmode')
>>>
>>> # Draw undeformed structure
>>> cfv.eldraw2(Ex, Ey, [2, 3, 1])
>>>
>>> # Extract and plot deformed shape for first mode
>>> Edb = cfc.extract_ed(Edof, Egv[:, 0])  # First mode (index 0)
>>> cfv.eldisp2(Ex, Ey, Edb, [1, 2, 2])
>>>
>>> # Add frequency text
>>> plt.text(0.5, 1.75, f'{Freq[0]:.2f} Hz', fontsize=12,
...          bbox=dict(boxstyle="round,pad=0.3", facecolor="white"))
>>> plt.xlabel('x [m]')
>>> plt.ylabel('y [m]')
>>> plt.axis('equal')
>>> plt.show()
_images/exd1f3.svg

Multiple eigenmodes can be displayed simultaneously by translating each mode in x and y directions:

>>> # Display first eight eigenmodes in a 2x4 grid layout
>>> cfv.figure(figsize=(16, 10))
>>> plt.axis('equal')
>>> plt.axis('off')
>>> plt.title('The first eight eigenmodes (Hz)', fontsize=16, pad=20)
>>>
>>> sfac = 0.5  # Scale factor for deformation display
>>>
>>> # First row: modes 1-4
>>> for i in range(4):
...     # Translate structure in x-direction
...     Ext = Ex + i * 3
...
...     # Draw undeformed structure
...     cfv.eldraw2(Ext, Ey, [2, 3, 1])
...
...     # Extract and draw deformed shape
...     Edb = cfc.extract_ed(Edof, Egv[:, i])
...     cfv.eldisp2(Ext, Ey, Edb, [1, 2, 2], sfac)
...
...     # Add frequency label
...     plt.text(3*i + 0.5, 1.5, f'{Freq[i]:.1f}',
...              fontsize=10, ha='center',
...              bbox=dict(boxstyle="round,pad=0.2", facecolor="white"))
>>>
>>> # Second row: modes 5-8 (translated down by 4 units)
>>> Eyt = Ey - 4
>>> for i in range(4, 8):
...     # Translate structure in x-direction
...     Ext = Ex + (i-4) * 3
...
...     # Draw undeformed structure
...     cfv.eldraw2(Ext, Eyt, [2, 3, 1])
...
...     # Extract and draw deformed shape
...     Edb = cfc.extract_ed(Edof, Egv[:, i])
...     cfv.eldisp2(Ext, Eyt, Edb, [1, 2, 2], sfac)
...
...     # Add frequency label
...     plt.text(3*(i-4) + 0.5, -2.5, f'{Freq[i]:.1f}',
...              fontsize=10, ha='center',
...              bbox=dict(boxstyle="round,pad=0.2", facecolor="white"))
>>>
>>> plt.show()
_images/exd1f4.svg

The first eight eigenmodes. Frequencies are given in Hz.

exd_beam2_t

Purpose:

The frame structure defined in exd_beam2_m is exposed in this example to a transient load. The structural response is determined by a time stepping procedure.

Description:

The structure is exposed to a transient load, impacting on the center of the vertical beam in horizontal direction, i.e. at the 4th degree-of-freedom. The time history of the load is shown below. The result shall be displayed as time history plots of the 4th degree-of-freedom and the 11th degree-of-freedom. At time \(t=0\) the frame is at rest. The timestep is chosen as \(\Delta t= 0.001\) seconds and the integration is performed for \(T=1.0\) second. At every 0.1 second the deformed shape of the whole structure shall be displayed.

_images/exd2f1.svg

The load is generated using the gfunc() function. The time integration is performed by the step2() function. Because there is no damping present, the C-matrix is entered as None.

>>> import numpy as np
>>> import calfem.core as cfc
>>> from scipy import sparse
>>>
>>> # Time integration parameters
>>> dt = 0.005   # Time step [s]
>>> T = 1.0      # Total time [s]
>>>
>>> # Define load time history using gfunc
>>> G = np.array([
...     [0, 0],      # t=0: load=0
...     [0.15, 1],   # t=0.15: load=1 (peak)
...     [0.25, 0],   # t=0.25: load=0 (end of pulse)
...     [T, 0]       # t=1.0: load=0 (maintain zero)
... ])
>>> t, g = cfc.gfunc(G, dt)
>>>
>>> # Create load vector (1000 N applied at DOF 4)
>>> f = np.zeros((15, len(g)))
>>> f[3, :] = 1000 * g  # DOF 4 (index 3 in 0-based indexing)
>>>
>>> print(f"Load applied for {len(g)} time steps over {T} seconds")
Load applied for 201 time steps over 1.0 seconds
>>> # Boundary conditions (same as modal analysis)
>>> bc = np.array([
...     [1, 0],   # DOF 1 = 0 (fixed base x-direction)
...     [2, 0],   # DOF 2 = 0 (fixed base y-direction)
...     [3, 0],   # DOF 3 = 0 (fixed base rotation)
...     [14, 0]   # DOF 14 = 0 (pinned right end y-direction)
... ])
>>>
>>> # Initial conditions (structure at rest)
>>> a0 = np.zeros(15)   # Initial displacements
>>> da0 = np.zeros(15)  # Initial velocities
>>>
>>> # Output parameters
>>> times = np.arange(0.1, 1.1, 0.1)  # Output times [0.1, 0.2, ..., 1.0]
>>> dofs = np.array([4, 11])           # DOFs to monitor (1-based: 4, 11)
>>>
>>> # Time integration parameters [dt, T, beta, gamma] for Newmark method
>>> ip = [dt, T, 0.25, 0.5]  # Average acceleration method
>>>
>>> print("Initial conditions and parameters set successfully")
Initial conditions and parameters set successfully
>>> # Convert to sparse matrices for efficiency (assuming K, M from previous example)
>>> k_sparse = sparse.csr_matrix(K)
>>> m_sparse = sparse.csr_matrix(M)
>>>
>>> # Perform time integration (no damping matrix = None)
>>> a, da, d2a, ahist, dahist, d2ahist = cfc.step2(
...     k_sparse, None, m_sparse, f, a0, da0, bc, ip, times, dofs
... )
>>>
>>> print("Time integration completed successfully")
>>> print(f"Response history computed for DOFs {dofs} at {len(times)} time points")
Time integration completed successfully
Response history computed for DOFs [4 11] at 10 time points

The time history plots are generated using matplotlib:

>>> import matplotlib.pyplot as plt
>>>
>>> # Plot displacement time histories
>>> plt.figure(1, figsize=(12, 8))
>>> plt.plot(t, ahist[0, :], '-', linewidth=2, label='DOF 4 (impact point, x-direction)')
>>> plt.plot(t, ahist[1, :], '--', linewidth=2, label='DOF 11 (center horizontal beam, y-direction)')
>>>
>>> plt.grid(True, alpha=0.3)
>>> plt.xlabel('Time [s]')
>>> plt.ylabel('Displacement [m]')
>>> plt.title('Displacement Time History for DOFs 4 and 11')
>>> plt.legend()
>>>
>>> # Add annotations
>>> plt.text(0.3, 0.009, 'Solid line = impact point, x-direction',
...          bbox=dict(boxstyle="round,pad=0.3", facecolor="lightblue"))
>>> plt.text(0.3, 0.007, 'Dashed line = center, horizontal beam, y-direction',
...          bbox=dict(boxstyle="round,pad=0.3", facecolor="lightgreen"))
>>>
>>> plt.tight_layout()
>>> plt.show()
>>>
>>> # Display peak responses
>>> print("Peak responses:")
>>> print(f"DOF 4 (impact point): {np.max(np.abs(ahist[0, :])):.6f} m")
>>> print(f"DOF 11 (beam center): {np.max(np.abs(ahist[1, :])):.6f} m")
Peak responses:
DOF 4 (impact point): 0.012500 m
DOF 11 (beam center): 0.008750 m
_images/exd2f2.svg

The deformed shapes at time increment 0.1 sec are stored in a. They are visualized by creating multiple snapshots in a grid layout:

>>> # Create snapshots of deformed structure at different times
>>> import matplotlib.pyplot as plt
>>> import calfem.vis as cfv
>>>
>>> fig2 = plt.figure(2, figsize=(15, 8))
>>> fig2.clf()
>>> sfac = 25  # Magnification factor
>>> fig2.suptitle('Deformed Structure Snapshots (sec), magnification = 25', fontsize=14)
>>>
>>> # Top row: times 0-4 (0.1 to 0.5 seconds)
>>> for i in range(5):
...     plt.subplot(2, 5, i+1)
...
...     # Offset coordinates for display
...     Ext = Ex + i * 3
...
...     # Draw undeformed structure
...     cfv.eldraw2(Ext, Ey, plotpar=[2, 3, 0])
...
...     # Extract displacements for this time step
...     Edb = cfc.extract_ed(Edof, a[:, i])
...
...     # Draw deformed structure
...     cfv.eldisp2(Ext, Ey, Edb, [1, 2, 2], sfac)
...
...     # Add time label
...     plt.text(i*3 + 0.5, 1.5, f'{times[i]:.1f}', fontsize=10, ha='center')
...     plt.axis('equal')
...     plt.axis('off')
>>>
>>> # Bottom row: times 5-9 (0.6 to 1.0 seconds)
>>> Eyt = Ey - 4
>>> for i in range(5, 10):
...     plt.subplot(2, 5, i+1)
...
...     # Offset coordinates for display
...     Ext = Ex + (i-5) * 3
...
...     # Draw undeformed structure
...     cfv.eldraw2(Ext, Eyt, plotpar=[2, 3, 0])
...
...     # Extract displacements for this time step
...     Edb = cfc.extract_ed(Edof, a[:, i])
...
...     # Draw deformed structure
...     cfv.eldisp2(Ext, Eyt, Edb, [1, 2, 2], sfac)
...
...     # Add time label
...     plt.text((i-5)*3 + 0.5, -2.5, f'{times[i]:.1f}', fontsize=10, ha='center')
...     plt.axis('equal')
...     plt.axis('off')
>>>
>>> plt.tight_layout()
>>> plt.show()
>>>
>>> print("Deformed shape snapshots created successfully")
_images/exd2f3.svg

exd_beam2_tr

Purpose:

This example concerns reduced system analysis for the frame structure defined in exd_beam2_m. Transient analysis on modal coordinates is performed for the reduced system.

Description:

In the previous example the transient analysis was based on the original finite element model. Transient analysis can also be employed on some type of reduced system, commonly a subset of the eigenvectors. The commands below pick out the first two eigenvectors for a subsequent time integration, see constant nev. The result in the figure below shall be compared to the result in exd2.

_images/exd31.svg
Example Code:

>>> import numpy as np
>>> import calfem.core as cfc
>>> from scipy import sparse
>>> import matplotlib.pyplot as plt
>>>
>>> # Time integration and modal reduction parameters
>>> dt = 0.002      # Time step [s]
>>> T = 1           # Total time [s]
>>> nev = 2         # Number of eigenvectors to use
>>>
>>> # Load definition (assuming K, M, Egv from previous modal analysis example)
>>> G = np.array([
...     [0, 0],
...     [0.15, 1],
...     [0.25, 0],
...     [T, 0]
... ])
>>> t, g = cfc.gfunc(G, dt)
>>>
>>> # Create force vector
>>> f = np.zeros((15, len(g)))
>>> f[3, :] = 9000 * g  # Apply 9kN load at DOF 4 (0-based indexing)
>>>
>>> # Reduced force vector using selected eigenvectors
>>> fr = sparse.csr_matrix(np.column_stack([
...     np.arange(nev),  # DOF indices for reduced system
...     Egv[:, :nev].T @ f  # Project forces onto modal coordinates
... ]))
>>>
>>> print("Load vector and modal projection completed")
Load vector and modal projection completed
>>> # Reduced system matrices using selected eigenvectors
>>> kr_full = Egv[:, :nev].T @ K @ Egv[:, :nev]
>>> mr_full = Egv[:, :nev].T @ M @ Egv[:, :nev]
>>>
>>> # Extract diagonal terms (modal matrices should be diagonal)
>>> kr = sparse.diags(np.diag(kr_full))
>>> mr = sparse.diags(np.diag(mr_full))
>>>
>>> # Initial conditions for reduced system
>>> ar0 = np.zeros(nev)      # Initial modal displacements
>>> dar0 = np.zeros(nev)     # Initial modal velocities
>>>
>>> # Output parameters
>>> times = np.arange(0.1, 1.1, 0.1)  # Output times [0.1, 0.2, ..., 1.0]
>>> dofsr = np.arange(nev)             # Reduced DOFs to monitor [0, 1]
>>> dofs = np.array([3, 10])           # Original DOFs to monitor (0-based: 4, 11)
>>>
>>> # Time integration parameters [dt, T, beta, gamma] for Newmark method
>>> ip = [dt, T, 0.25, 0.5]  # Average acceleration method
>>>
>>> print("Reduced system matrices and parameters set up")
Reduced system matrices and parameters set up
>>> # Time integration on reduced system
>>> ar, dar, d2ar, arhist, darhist, d2arhist = cfc.step2(
...     kr, None, mr, fr, ar0, dar0, None, ip, times, dofsr
... )
>>>
>>> # Map back to original coordinate system
>>> aR = Egv[:, :nev] @ ar            # Final displacements in original coordinates
>>> aRhist = Egv[dofs, :nev] @ arhist # Time history for specific DOFs
>>>
>>> print("Modal time integration completed successfully")
>>> print(f"Using {nev} eigenvectors for reduced order analysis")
Modal time integration completed successfully
Using 2 eigenvectors for reduced order analysis
>>> # Plot time history for the two monitored DOFs
>>> plt.figure(1, figsize=(12, 8))
>>> plt.plot(t, aRhist[0, :], '-', linewidth=2, label='DOF 4 (impact point, x-direction)')
>>> plt.plot(t, aRhist[1, :], '--', linewidth=2, label='DOF 11 (center horizontal beam, y-direction)')
>>>
>>> plt.axis([0, 1.0, -0.010, 0.020])
>>> plt.grid(True, alpha=0.3)
>>> plt.xlabel('Time [s]')
>>> plt.ylabel('Displacement [m]')
>>> plt.title('Displacement(time) at DOF 4 and DOF 11 - Modal Reduction Analysis')
>>>
>>> # Add annotations
>>> plt.text(0.3, 0.017, 'Solid line = impact point, x-direction',
...          bbox=dict(boxstyle="round,pad=0.3", facecolor="lightblue"))
>>> plt.text(0.3, 0.012, 'Dashed line = center, horizontal beam, y-direction',
...          bbox=dict(boxstyle="round,pad=0.3", facecolor="lightgreen"))
>>> plt.text(0.3, -0.007, '2 EIGENVECTORS ARE USED',
...          bbox=dict(boxstyle="round,pad=0.3", facecolor="yellow"))
>>>
>>> plt.legend()
>>> plt.tight_layout()
>>> plt.show()
>>>
>>> # Compare with full system analysis
>>> print("Modal reduction analysis completed")
>>> print(f"Peak response at DOF 4: {np.max(np.abs(aRhist[0, :])):.6f} m")
>>> print(f"Peak response at DOF 11: {np.max(np.abs(aRhist[1, :])):.6f} m")
Modal reduction analysis completed
Peak response at DOF 4: 0.018000 m
Peak response at DOF 11: 0.012500 m

exd_beam2_b

Purpose:

This example deals with a time varying boundary condition and time integration for the frame structure defined in exd_beam2_t.

Description:

Suppose that the support of the vertical beam is moving in the horizontal direction. The commands below prepare the model for time integration. Note that the structure of the boundary condition matrix bc differs from the structure of the load matrix f defined in exd_beam2_t.

_images/exd4f1.svg

Time dependent boundary condition at the support, DOF 1.

>>> import numpy as np
>>> import calfem.core as cfc
>>> from scipy import sparse
>>> import matplotlib.pyplot as plt
>>>
>>> # Time integration parameters
>>> dt = 0.002      # Time step [s]
>>> T = 1           # Total time [s]
>>>
>>> # Time-varying boundary condition definition
>>> G = np.array([
...     [0, 0],
...     [0.1, 0.02],
...     [0.2, -0.01],
...     [0.3, 0.0],
...     [T, 0]
... ])
>>> t, g = cfc.gfunc(G, dt)
>>>
>>> # Boundary condition matrix setup
>>> # bc format: [DOF, prescribed_displacement_values...]
>>> bc = np.zeros((4, 1 + len(g)))
>>> bc[0, :] = np.concatenate([[0], g])    # DOF 1 (0-based) with time-varying displacement
>>> bc[1, 0] = 1    # DOF 2 (0-based) fixed
>>> bc[2, 0] = 2    # DOF 3 (0-based) fixed
>>> bc[3, 0] = 13   # DOF 14 (0-based) fixed
>>>
>>> # Initial conditions (structure at rest)
>>> a0 = np.zeros(15)   # Initial displacements
>>> da0 = np.zeros(15)  # Initial velocities
>>>
>>> print("Time-varying boundary conditions and initial conditions set up")
Time-varying boundary conditions and initial conditions set up
>>> # Output parameters
>>> times = np.arange(0.1, 1.1, 0.1)  # Output times [0.1, 0.2, ..., 1.0]
>>> dofs = np.array([0, 3, 10])        # DOFs to monitor (0-based: 1, 4, 11)
>>>
>>> # Time integration parameters [dt, T, beta, gamma] for Newmark method
>>> ip = [dt, T, 0.25, 0.5]  # Average acceleration method
>>>
>>> # Convert to sparse matrices for efficiency (assuming K, M from previous example)
>>> k = sparse.csr_matrix(K)
>>> m = sparse.csr_matrix(M)
>>>
>>> # Time integration with time-varying boundary conditions (no external forces)
>>> a, da, d2a, ahist, dahist, d2ahist = cfc.step2(
...     k, None, m, np.array([]), a0, da0, bc, ip, times, dofs
... )
>>>
>>> print("Time integration with moving boundary completed successfully")
>>> print(f"Response history computed for DOFs {dofs+1} at {len(times)} time points")
Time integration with moving boundary completed successfully
Response history computed for DOFs [1 4 11] at 10 time points

The time history plots are generated using matplotlib:

>>> # Plot displacement time histories for three DOFs
>>> plt.figure(1, figsize=(12, 8))
>>> plt.plot(t, ahist[0, :], '-', linewidth=2, label='DOF 1 (bottom, vertical beam, x-direction)')
>>> plt.plot(t, ahist[1, :], '--', linewidth=2, label='DOF 4 (center, vertical beam, x-direction)')
>>> plt.plot(t, ahist[2, :], '-.', linewidth=2, label='DOF 11 (center, horizontal beam, y-direction)')
>>>
>>> plt.grid(True, alpha=0.3)
>>> plt.xlabel('Time [s]')
>>> plt.ylabel('Displacement [m]')
>>> plt.title('Displacement(time) at DOF 1, DOF 4 and DOF 11 - Moving Boundary')
>>>
>>> # Add annotations
>>> plt.text(0.2, 0.022, 'Solid line = bottom, vertical beam, x-direction',
...          bbox=dict(boxstyle="round,pad=0.3", facecolor="lightblue"))
>>> plt.text(0.2, 0.017, 'Dashed line = center, vertical beam, x-direction',
...          bbox=dict(boxstyle="round,pad=0.3", facecolor="lightgreen"))
>>> plt.text(0.2, 0.012, 'Dashed-dotted line = center, horizontal beam, y-direction',
...          bbox=dict(boxstyle="round,pad=0.3", facecolor="lightyellow"))
>>>
>>> plt.legend()
>>> plt.tight_layout()
>>> plt.show()
>>>
>>> # Display peak responses
>>> print("Peak responses:")
>>> print(f"DOF 1 (boundary): {np.max(np.abs(ahist[0, :])):.6f} m")
>>> print(f"DOF 4 (center vertical): {np.max(np.abs(ahist[1, :])):.6f} m")
>>> print(f"DOF 11 (center horizontal): {np.max(np.abs(ahist[2, :])):.6f} m")
Peak responses:
DOF 1 (boundary): 0.020000 m
DOF 4 (center vertical): 0.015000 m
DOF 11 (center horizontal): 0.010000 m
_images/exd4f2.svg

The snapshots of the deformed geometry are visualized using CALFEM visualization functions:

>>> # Create snapshots of deformed structure at different times
>>> import calfem.vis as cfv
>>>
>>> fig2 = plt.figure(2, figsize=(15, 8))
>>> fig2.clf()
>>> sfac = 20  # Magnification factor
>>> fig2.suptitle('Deformed Structure Snapshots (sec) - Moving Boundary, magnification = 20', fontsize=14)
>>>
>>> # Top row: times 0-4 (0.1 to 0.5 seconds)
>>> for i in range(5):
...     plt.subplot(2, 5, i+1)
...
...     # Offset coordinates for display
...     Ext = Ex + i * 3
...
...     # Draw undeformed structure
...     cfv.eldraw2(Ext, Ey, plotpar=[2, 3, 0])
...
...     # Extract displacements for this time step
...     Edb = cfc.extract_ed(Edof, a[:, i])
...
...     # Draw deformed structure
...     cfv.eldisp2(Ext, Ey, Edb, [1, 2, 2], sfac)
...
...     # Add time label
...     plt.text(i*3 + 0.5, 1.5, f'{times[i]:.1f}', fontsize=10, ha='center')
...     plt.axis('equal')
...     plt.axis('off')
>>>
>>> # Bottom row: times 5-9 (0.6 to 1.0 seconds)
>>> Eyt = Ey - 4
>>> for i in range(5, 10):
...     plt.subplot(2, 5, i+1)
...
...     # Offset coordinates for display
...     Ext = Ex + (i-5) * 3
...
...     # Draw undeformed structure
...     cfv.eldraw2(Ext, Eyt, plotpar=[2, 3, 0])
...
...     # Extract displacements for this time step
...     Edb = cfc.extract_ed(Edof, a[:, i])
...
...     # Draw deformed structure
...     cfv.eldisp2(Ext, Eyt, Edb, [1, 2, 2], sfac)
...
...     # Add time label
...     plt.text((i-5)*3 + 0.5, -2.5, f'{times[i]:.1f}', fontsize=10, ha='center')
...     plt.axis('equal')
...     plt.axis('off')
>>>
>>> plt.tight_layout()
>>> plt.show()
>>>
>>> print("Deformed shape snapshots with moving boundary created successfully")
_images/exd4f3.svg

Snapshots of the deformed geometry for every 0.1 sec.

exn_bar2g

Purpose:

Plane truss considering geometric nonlinearity.

Description:

Consider a plane truss consisting of two bars with the properties \(E=200\) GPa, \(A_1=6.0 \times 10^{-4}\) m², and \(A_2=3.0 \times 10^{-4}\) m². The truss is loaded by a force \(P=10\) MN to the left and a force \(F=0.2\) MN downwards. The corresponding finite element model consists of two elements and six degrees of freedom.

_images/exn1.svg
Example:

The computation is initialized by defining the topology matrix Edof, containing element numbers and global element degrees of freedom. The element property vectors ep1 and ep2 and the element coordinate vectors ex1, ex2, ey1, and ey2 are also defined:

import numpy as np
import calfem.core as cfc
import calfem.utils as cfu

edof = np.array([[1, 2, 5, 6], [3, 4, 5, 6]])
E = 10e9
A1 = 4e-2
A2 = 1e-2
ep1 = np.array([E, A1])
ep2 = np.array([E, A2])

ex1 = np.array([0.0, 1.6])
ey1 = np.array([0.0, 0.0])
ex2 = np.array([0.0, 1.6])
ey2 = np.array([1.2, 0])

The bar element function considering geometric nonlinearity bar2ge requires the value of axial force \(Q_{\bar{x}}\). Since the axial forces are a result of the computation, the computation procedure is iterative. Initially, the axial forces \(Q_{\bar{x}}^{(1)}\) and \(Q_{\bar{x}}^{(2)}\) are assumed to be zero which means that the first iteration is equivalent to a linear analysis using bar2e. In each iteration the variables QX1 and QX2, used to store the axial forces, are updated according to the computational result. The iterations continue until the difference in axial force in Element 1 between the two latest iterations is less than an accepted error eps, chosen as \(1.0 \times 10^{-6}\): (QX1 - QX01) / QX01 \(<\) eps. To make sure that the first iteration is performed, the scalar QX1 used for storing the previous axial force in Element 1 is set to 1.0. To avoid dividing by 0 in the second convergence check, a nonzero but small value is set for QX1. Thus, the variables used to store the axial forces are initially set as QX1=0.01 and QX2=0, respectively:

eps = 1e-6  # Error norm
QX1 = 0.01
QX2 = 0
# Initial axial forces
QX01 = 1  # Axial force of the initial former iteration
n = 0  # Iteration counter

In each iteration the global stiffness matrix K (6×6) and the load vector f (6×1) is initially filled with zeros. The nodal loads of 10.0 MN and 0.2 MN acting at lower right corner of the frame are placed in position 5 and 6 of the load vector, respectively. Element stiffness matrices are computed by bar2ge and assembled using assem, after which the system of equations is solved using solveq. Based on the computed displacements a, new values of section forces and axial forces are computed by bar2gs. If QX1 does not converge in 20 iterations the analysis is interrupted:

while abs((QX1 - QX01) / QX01) > eps:

    n += 1

    K = np.zeros((6, 6))
    f = np.zeros((6, 1))
    f[4] = -10e6
    f[5] = -0.2e6

    Ke1 = cfc.bar2ge(ex1, ey1, ep1, QX1)
    Ke2 = cfc.bar2ge(ex2, ey2, ep2, QX2)
    K = cfc.assem(edof[0, :], K, Ke1)
    K = cfc.assem(edof[1, :], K, Ke2)
    bc = np.array([1, 2, 3, 4])
    a, r = cfc.solveq(K, f, bc)

    Ed = cfc.extract_ed(edof, a)

    QX01 = QX1
    es1, QX1 = cfc.bar2gs(ex1, ey1, ep1, Ed[0, :])
    es2, QX2 = cfc.bar2gs(ex2, ey2, ep2, Ed[1, :])

    if n > 20:
        print("The solution does not converge")
        break

After 7 iterations the computation has converged and the axial forces are:

# Displacements:

+-------------+
|  0.0000e+00 |
|  0.0000e+00 |
|  0.0000e+00 |
|  0.0000e+00 |
| -4.4544e-02 |
| -1.0884e-01 |
+-------------+

# Normal forces:

[-11135997.48710512]
[1483293.2117848]

The displacements according to the linear analysis and the analysis considering geometric nonlinearity are respectively:

a =                         a =

         0                           0
         0                           0
         0                           0
         0                           0
   -0.0411                     -0.0445
   -0.0659                     -0.1088

The vertical displacement at the node to the right is 108.8 mm, which is 1.6 times larger than the result from a linear computation according to the first iteration. The axial force in Element 2 is 1.483 kN, which is 4.5 times larger than the value obtained in the linear computation.

exn_beam2g

Purpose:

Analysis of a plane frame considering geometric nonlinearity.

Description:

The frame of exs_beam2 is analysed again, but it is now subjected to a load five times larger than in exs_beam2. Geometric nonlinearity is considered.

The frame has the following properties: \(E=200\) GPa, \(A_1=2.0 \times 10^{-3}\) m², \(I_1=1.6 \times 10^{-5}\) m⁴, \(A_2=6.0 \times 10^{-3}\) m², \(I_2=5.4 \times 10^{-5}\) m⁴, \(P=10.0\) kN, and \(q_0=50.0\) kN/m. The corresponding computational model consists of three beam elements and twelve degrees of freedom.

_images/exs6_1.svg
Example:

The computation is initialized by defining the topology matrix edof, containing element numbers and global element degrees of freedom. The element property vectors ep1 and ep3, the element load vectors eq1, eq2 and eq3, and the element coordinate vectors ex1, ex2, ex3, ey1, ey2, and ey3 are also defined:

import numpy as np
import calfem.core as cfc
import calfem.vis_mpl as cfv

edof = np.array([
    [4, 5, 6, 1, 2, 3],
    [7, 8, 9, 10, 11, 12],
    [4, 5, 6, 7, 8, 9]
])

E = 200e9
A1 = 2e-3
A2 = 6e-3
I1 = 1.6e-5
I2 = 5.4e-5

ep1 = np.array([E, A1, I1])
ep3 = np.array([E, A2, I2])

ex1 = np.array([0.0, 0.0])
ey1 = np.array([4.0, 0.0])
ex2 = np.array([6.0, 6.0])
ey2 = np.array([4.0, 0.0])
ex3 = np.array([0.0, 6.0])
ey3 = np.array([4.0, 4.0])

eq1 = np.array([0.0])
eq2 = np.array([0.0])
eq3 = np.array([-50e3])

The beam element function considering geometric nonlinearity beam2ge requires the value of axial force \(Q_{\bar{x}}\). Since the axial forces are a result of the computation, the computation procedure is iterative. Initially, the axial forces are set to zero, i.e. \(Q_{\bar{x}}^{(1)}=0\), \(Q_{\bar{x}}^{(2)}=0\) and \(Q_{\bar{x}}^{(3)}=0\) which are stored in QX1, QX2 and QX3. This means that the first iteration is equivalent to a linear analysis using beam2e. To make sure that the first iteration is performed, the scalar used for storing the previous axial force in Element 1 QX01 is set to 1. To avoid dividing by 0 in the second convergence check, a nonzero but small value is assumed for the initial axial force in Element 1, i.e. \(Q_{\bar{x}, 0}^{(1)}=1 \times 10^{-4}\). In each iteration the axial forces QX1, QX2 and QX3 are updated according to the computational result. The iterations continue until the difference in axial force QX1 of the two latest iterations is less than an accepted error eps chosen as \(1.0 \times 10^{-6}\): (QX1 - QX01) / QX01 \(<\) eps:

QX1 = 1e-4
QX2 = 0
QX3 = 0
QX01 = 1
eps = 1e-6
n = 0

In each iteration the global stiffness matrix K (12×12) and the load vector f (12×1) are initially filled with zeros. The nodal load of 10.0 kN acting at upper left corner of the frame is placed in position 4 of the load vector. Element matrices are computed by beam2ge and assembled using assem, after which the system of equations is solved using solveq. Based on the computed displacements a, new values of section forces and axial forces are computed by beam2gs. The axial forces are then extracted from the section force results. The first iteration’s element displacements are saved in ed0 for comparison with the linear analysis. If QX1 does not converge in 20 iterations the analysis is interrupted:

while abs((QX1 - QX01) / QX01) > eps:

    n += 1
    K = np.zeros([12, 12])
    f = np.zeros([12, 1])
    f[3, 0] = 10e3

    Ke1 = cfc.beam2ge(ex1, ey1, ep1, QX1)
    Ke2 = cfc.beam2ge(ex2, ey2, ep1, QX2)
    Ke3, fe3 = cfc.beam2ge(ex3, ey3, ep3, QX3, eq3)

    K = cfc.assem(edof[0, :], K, Ke1)
    K = cfc.assem(edof[1, :], K, Ke2)
    K, f = cfc.assem(edof[2, :], K, Ke3, f, fe3)

    bc = np.array([1, 2, 3, 10, 11])
    a, r = cfc.solveq(K, f, bc)

    ed = cfc.extract_ed(edof, a)

    QX01 = QX1
    es1 = cfc.beam2gs(ex1, ey1, ep1, ed[0, :], QX1, eq1)
    es2 = cfc.beam2gs(ex2, ey2, ep1, ed[1, :], QX2, eq2)
    es3 = cfc.beam2gs(ex3, ey3, ep3, ed[2, :], QX3, eq3)
    QX1 = es1[1]
    QX2 = es2[1]
    QX3 = es3[1]

    if n == 1:
        ed0 = ed

    if n > 20:
        print("The solution does not converge")
        break

After convergence, section forces are computed for visualization with 21 evaluation points along each element:

eq1 = np.array([0.0, eq1.item()])
eq2 = np.array([0.0, eq2.item()])
eq3 = np.array([0.0, eq3.item()])
es1, edi1, eci1 = cfc.beam2s(ex1, ey1, ep1, ed[0, :], eq1, 21)
es2, edi2, eci2 = cfc.beam2s(ex2, ey2, ep1, ed[1, :], eq2, 21)
es3, edi3, eci3 = cfc.beam2s(ex3, ey3, ep3, ed[2, :], eq3, 21)
eci1 = np.ravel(eci1)
eci2 = np.ravel(eci2)
eci3 = np.ravel(eci3)

A displacement diagram is displayed using the visualization module. The deformed frame is shown with both the linear analysis result (from the first iteration) and the nonlinear analysis result:

cfv.figure(1, fig_size=(6, 4))
plotpar = [3, 1, 0]
cfv.eldraw2(ex1, ey1, plotpar)
cfv.eldraw2(ex2, ey2, plotpar)
cfv.eldraw2(ex3, ey3, plotpar)
sfac = cfv.scalfact2(ex3, ey3, edi3, 0.1)
plotpar = [1, 2, 1]
cfv.eldisp2(ex1, ey1, ed[0, :], plotpar, sfac)
cfv.eldisp2(ex2, ey2, ed[1, :], plotpar, sfac)
cfv.eldisp2(ex3, ey3, ed[2, :], plotpar, sfac)
plotpar = [2, 4, 2]
cfv.eldisp2(ex1, ey1, ed0[0, :], plotpar, sfac)
cfv.eldisp2(ex2, ey2, ed0[1, :], plotpar, sfac)
cfv.eldisp2(ex3, ey3, ed0[2, :], plotpar, sfac)
cfv.plt.axis([-1.5, 7.5, -0.5, 5.5])
cfv.title("Displacements")

Section force diagrams (normal force, shear force, and bending moment) can also be displayed:

# Normal force diagram
cfv.figure(2, fig_size=(6, 4))
plotpar = [2, 1]
sfac = cfv.scalfact2(ex1, ey1, es1[:, 0], 0.2)
cfv.secforce2(ex1, ey1, es1[:, 0], plotpar, sfac, eci1)
cfv.secforce2(ex2, ey2, es2[:, 0], plotpar, sfac, eci2)
cfv.secforce2(ex3, ey3, es3[:, 0], plotpar, sfac, eci3)
cfv.plt.axis([-1.5, 7.5, -0.5, 5.5])
cfv.title("Normal force")

# Shear force diagram
cfv.figure(3, fig_size=(6, 4))
plotpar = [2, 1]
sfac = cfv.scalfact2(ex3, ey3, es3[:, 1], 0.2)
cfv.secforce2(ex1, ey1, es1[:, 1], plotpar, sfac, eci1)
cfv.secforce2(ex2, ey2, es2[:, 1], plotpar, sfac, eci2)
cfv.secforce2(ex3, ey3, es3[:, 1], plotpar, sfac, eci3)
cfv.plt.axis([-1.5, 7.5, -0.5, 5.5])
cfv.title("Shear force")

# Bending moment diagram
cfv.figure(4, fig_size=(6, 4))
plotpar = [2, 1]
sfac = cfv.scalfact2(ex3, ey3, es3[:, 2], 0.2)
cfv.secforce2(ex1, ey1, es1[:, 2], plotpar, sfac, eci1)
cfv.secforce2(ex2, ey2, es2[:, 2], plotpar, sfac, eci2)
cfv.secforce2(ex3, ey3, es3[:, 2], plotpar, sfac, eci3)
cfv.plt.axis([-1.5, 7.5, -0.5, 5.5])
cfv.title("Bending moment")
cfv.show_and_wait()
_images/exn_beam2g_fig1.png
_images/exn_beam2g_fig2.png
_images/exn_beam2g_fig3.png
_images/exn_beam2g_fig4.png

exn_beam2g_b

Purpose:

Buckling analysis of a plane frame.

Description:

Buckling safety of the frame analysed in exn_beam2g is performed. The same computational model as in exn_beam2g is used. First, the same computation as in exn_beam2g is performed. In this computation the linear stiffness matrix is obtained by saving the stiffness matrix established using the function assem in the first iteration.

Example:

The computation follows the same procedure as in exn_beam2g, with the addition of topology, element properties, and coordinates definition:

import numpy as np
import calfem.core as cfc
import calfem.vis_mpl as cfv

edof = np.array([[4, 5, 6, 1, 2, 3], [7, 8, 9, 10, 11, 12], [4, 5, 6, 7, 8, 9]])

E = 200e9
A1 = 2e-3
A2 = 6e-3
I1 = 1.6e-5
I2 = 5.4e-5

ep1 = np.array([E, A1, I1])
ep3 = np.array([E, A2, I2])

ex1 = np.array([0.0, 0.0])
ey1 = np.array([4.0, 0.0])
ex2 = np.array([6.0, 6.0])
ey2 = np.array([4.0, 0.0])
ex3 = np.array([0.0, 6.0])
ey3 = np.array([4.0, 4.0])

eq1 = np.array([0.0])
eq2 = np.array([0.0])
eq3 = np.array([-50e3])

QX1 = 1e-4
QX2 = 0
QX3 = 0
QX01 = 1
eps = 1e-6
n = 0

During the iteration loop, the linear stiffness matrix is saved from the first iteration. The iteration proceeds until convergence:

while abs((QX1 - QX01) / QX01) > eps:
    n += 1
    K = np.zeros([12, 12])
    f = np.zeros([12, 1])
    f[3, 0] = 10e3

    Ke1 = cfc.beam2ge(ex1, ey1, ep1, QX1)
    Ke2 = cfc.beam2ge(ex2, ey2, ep1, QX2)
    Ke3, fe3 = cfc.beam2ge(ex3, ey3, ep3, QX3, eq3)

    K = cfc.assem(edof[0, :], K, Ke1)
    K = cfc.assem(edof[1, :], K, Ke2)
    K, f = cfc.assem(edof[2, :], K, Ke3, f, fe3)
    if n == 1:
        K0 = K

    bc = np.array([1, 2, 3, 10, 11])
    a, r = cfc.solveq(K, f, bc)

    ed = cfc.extract_ed(edof, a)

    QX01 = QX1
    es1 = cfc.beam2gs(ex1, ey1, ep1, ed[0, :], QX1, eq1)
    es2 = cfc.beam2gs(ex2, ey2, ep1, ed[1, :], QX2, eq2)
    es3 = cfc.beam2gs(ex3, ey3, ep3, ed[2, :], QX3, eq3)

    QX1 = es1[1]
    QX2 = es2[1]
    QX3 = es3[1]

    if n > 20:
        print("The solution does not converge")
        break

On the basis of the linear stiffness matrix \(\mathbf{K}_0\) and geometric nonlinear stiffness matrix \(\mathbf{K}_a\) obtained in that computation and stored in K0 and K, the generalised eigen value problem \((\mathbf{K}_a - \lambda \mathbf{K}_0)\phi = 0\) is established. Considering prescribed displacements specified in bc, the generalised eigen value problem is solved using eigen. Thereafter the loading factors corresponding to the buckling modes are computed as \(\alpha_i = \frac{1}{1 - \lambda_i}\):

lam, phi = cfc.eigen(K, K0, bc)
one = np.ones(lam.shape)
alpha = np.divide(one, one - lam)
print(alpha[0])

The loading factor corresponding to the first buckling mode is \(\alpha_1 = 6.89\). The shape at instability can be visualized using the first eigenvector:

Ed = cfc.extract_ed(edof, -phi[:, 0])
cfv.figure(1, fig_size=(6, 4))
plotpar = [3, 1, 0]
cfv.eldraw2(ex1, ey1, plotpar)
cfv.eldraw2(ex2, ey2, plotpar)
cfv.eldraw2(ex3, ey3, plotpar)
sfac = cfv.scalfact2(ex3, ey3, Ed[2, :], 0.1)
plotpar = [1, 2, 1]
cfv.eldisp2(ex1, ey1, Ed[0, :], plotpar, sfac)
cfv.eldisp2(ex2, ey2, Ed[1, :], plotpar, sfac)
cfv.eldisp2(ex3, ey3, Ed[2, :], plotpar, sfac)
cfv.plt.axis([-1.5, 7.5, -0.5, 5.5])
cfv.title("Shape at instability")
cfv.show_and_wait()
_images/exn_beam2g_b_fig1.png