EEM: Based on the examples in the book "Finite-element method for beams".

By Johan Blaauwendraad http://www.delftacademicpress.nl/f040.php (in Dutch).

This tutorial notebook uses the figure 1.1 example in EEM and explanations how this can implemented using the CSoM package.


In [1]:
#Pkg.update() # Only needed if Julia is not up to date.

In [2]:
using CSoM, DataFrames, Plots

CSoM.jl contains the Julia version of the Fortran toolkit as described in the book "Programming the Finite Element Method" ("PtFEM") by I. M. Smith, D. V. Griffiths & L. Margetts (5th edition).

The Julia DataFrames package is used to display the final results. The Julai Plots package is used to plot the results, in this notebook using the GR backend. See https://juliaplots.github.io

Currently in CSoM, the examples in PtFEM consists of 3 Julia components:

  1. The input data, e.g. for the example defined in p4.1.1.jl. In this tutorial notebook the contents of p4.1.1 is re-created. The p4.1.1.jl Julia file itself can be found in the "examples/4 Static Equilibrium" subdirectory of the CSoM.jl package.

  2. A program skeleton as described in the PtFEM book, e.g. Program 4.1 is FE4_1.jl in subdirectory "src/Chap04" and can be included accordingly (see below).

  3. The skeleton programs call the appropriate Julia functions in subdirectory "src/CSoM".


In [3]:
path = joinpath(Pkg.dir("CSoM"), "src", "Chap04")
# If FE4_1 already loaded, skip the include.
if !isdefined(Main, :FE4_1)
    include(path*"/FE4_1.jl")
end

Thus this tutorial notebook consists of 2 parts:

1. Show how to create the contents of p4.1.1.jl and use this as input to the included FE4_1.jl function.

2. Step by step explanation what happens in FE4_1.

Note: FE4_1.jl, the skeleton program used in this example, is an almost straight translation from Fortran in PtFEM to Julia in CSoM.jl (with the exception of the program input and output sections). This is true for all FEx_x.jl files.

Define the structural element type and the finite element used to create a mesh over the structural element.


In [4]:
struct_el = :Rod   # A 1-dimensional structural element that can only handle axial forces.
fin_el = :Line     # The type of finite element used to construct a mesh for the structural element;

Define the overall parameters


In [5]:
l = 1.0       # Total length of structural element [m]
q = 5.0       # Distributed load [N/m]
N = 10        # Number of nodes
els = N - 1   # Number of finite elements
nod = 2       # Number of nodes per finite elements
nodof = 1     # Degrees of freedom for each node, just axial ;

Create input dictionary


In [6]:
data = Dict(
  # Rod(nxe, np_types, nip, fin_el(nod, nodof))
  :struc_el => getfield(Main, Symbol(struct_el))(els, 1, 1,
    getfield(Main, Symbol(fin_el))(nod, nodof)),
  :properties => [1.0e5;],
  :x_coords => 0.0:l/els:l,
  :support => [(N, [0])],
  :loaded_nodes => [(i, repeat([0.0], inner=nodof)) for i in 1:N],
  :eq_nodal_forces_and_moments => [(i, repeat([0.0], inner=nodof*nod)) for i in 1:els]
);

In this example there are only distributed loads. Otherwise set data[:loaded_nodes] to the external forces and moments directly applied to nodes, e.g.:

data[:loaded_nodes][2] = (2, [5.0])

Assign the distributed load contribution to nodal forces


In [7]:
for node in 1:els
  data[:eq_nodal_forces_and_moments][node][2][1] = (1/2)*q*l/els
  data[:eq_nodal_forces_and_moments][node][2][2] = (1/2)*q*l/els
end

Add the equivalent distributed forces and moment to existing loaded_nodes entry in data.


In [8]:
for node in 1:N-1
  data[:loaded_nodes][node][2][1] += data[:eq_nodal_forces_and_moments][node][2][1]
  data[:loaded_nodes][node+1][2][1] += data[:eq_nodal_forces_and_moments][node][2][2]
end

In [9]:
data


Out[9]:
Dict{Symbol,Any} with 6 entries:
  :eq_nodal_forces_and_moments => Tuple{Int64,Array{Float64,1}}[(1,[0.277778,0.…
  :support                     => Tuple{Int64,Array{Int64,1}}[(10,[0])]
  :struc_el                    => CSoM.Rod(9,1,1,CSoM.Line(2,1))
  :properties                  => [100000.0]
  :x_coords                    => 0.0:0.1111111111111111:1.0
  :loaded_nodes                => Tuple{Int64,Array{Float64,1}}[(1,[0.277778]),…

Notice that in this 'flagpole' example the x axis goes from top to bottom and the distributed load is compressive. The clamped ('encastré') end node is node N.

Now call the FE4.1 skeleton


In [10]:
m = FE4_1(data);


There are 9 equations and the skyline storage is 17.


In [11]:
@time m = FE4_1(data);


There are 9 equations and the skyline storage is 17.

  0.000916 seconds (674 allocations: 51.172 KB)

In [12]:
m.displacements


Out[12]:
10×1 Array{Float64,2}:
 2.5e-5    
 2.46914e-5
 2.37654e-5
 2.22222e-5
 2.00617e-5
 1.7284e-5 
 1.38889e-5
 9.87654e-6
 5.24691e-6
 0.0       

Internal element actions, not correctred for applied equivalent nodal forces and moments, this will be done below


In [13]:
m.actions


Out[13]:
9×2 Array{Float64,2}:
 0.277778  -0.277778
 0.833333  -0.833333
 1.38889   -1.38889 
 1.94444   -1.94444 
 2.5       -2.5     
 3.05556   -3.05556 
 3.61111   -3.61111 
 4.16667   -4.16667 
 4.72222   -4.72222 

Reconstruct stiffness matrix from skyline vector


In [14]:
fromSkyline(m.kv, m.kdiag)


Out[14]:
9×9 Array{Float64,2}:
  948.683  -948.683     0.0       0.0    …     0.0       0.0       0.0  
 -948.683   948.683  -948.683     0.0          0.0       0.0       0.0  
    0.0    -948.683   948.683  -948.683        0.0       0.0       0.0  
    0.0       0.0    -948.683   948.683        0.0       0.0       0.0  
    0.0       0.0       0.0    -948.683        0.0       0.0       0.0  
    0.0       0.0       0.0       0.0    …  -948.683     0.0       0.0  
    0.0       0.0       0.0       0.0        948.683  -948.683     0.0  
    0.0       0.0       0.0       0.0       -948.683   948.683  -948.683
    0.0       0.0       0.0       0.0          0.0    -948.683   948.683

Create the result dataframes.

Dataframe dis_df holds the node displacements (deflections and rotations).

Dataframe fm_df holds the element actions (forces and moments).


In [15]:
dis_df = DataFrame(
x_deflections = m.displacements[:, 1],
)

fm_df = DataFrame(
    normal_force_1 = m.actions[:, 1],
    normal_force_2 = m.actions[:, 2],
);

Correct element forces and moments for equivalent nodal forces and moments introduced for loading between nodes


In [16]:
if :eq_nodal_forces_and_moments in keys(data)
    eqfm = data[:eq_nodal_forces_and_moments]
    k = data[:struc_el].fin_el.nod * data[:struc_el].fin_el.nodof
    for t in eqfm
      for i in 1:k
            fm_df[t[1], i] -= round(t[2][i], 2)
      end
    end
end

In [17]:
dis_df


Out[17]:
x_deflections
12.5000000000000018e-5
22.4691358024691377e-5
32.3765432098765453e-5
42.222222222222224e-5
52.0061728395061748e-5
61.728395061728397e-5
71.3888888888888906e-5
89.876543209876555e-6
95.246913580246922e-6
100.0

In [18]:
fm_df


Out[18]:
normal_force_1normal_force_2
1-0.0022222222222214594-0.5577777777777786
20.5533333333333321-1.1133333333333322
31.1088888888888893-1.6688888888888893
41.6644444444444428-2.224444444444443
52.2199999999999998-2.7800000000000002
62.7755555555555587-3.335555555555559
73.331111111111114-3.8911111111111145
83.886666666666671-4.446666666666672
94.442222222222228-5.002222222222229

In [19]:
gr(size=(400,600))


Out[19]:
Plots.GRBackend()

In [20]:
p = Vector{Plots.Plot{Plots.GRBackend}}(2)
titles = ["EEM fig 1.1 u(x)", "EEM fig 1.1 N(x)"]
fors = vcat(fm_df[:, :normal_force_1], q*l);

In [21]:
p[1] = plot(data[:x_coords], -fors,
    xlim=(0,l), ylim=(-6.0, 0.0),
    xlabel="x [m]", ylabel="Normal force [N]", color=:blue,
    line=(:dash,1), marker=(:dot,1,0.8,stroke(1,:black)),
    title=titles[2], leg=false
)


Out[21]:
0.0 0.2 0.4 0.6 0.8 1.0 -6 -5 -4 -3 -2 -1 0 EEM fig 1.1 N(x) x [m] Normal force [N]

In [22]:
p[2] = plot(data[:x_coords], m.displacements[:, 1],
    xlim=(0, l), ylim=(0.0, 0.00003),
    xlabel="x [m]", ylabel="Deflection [m]", color=:red,
    line=(:dash,1), marker=(:circle,1,0.8,stroke(1,:black)),
    title=titles[1], leg=false
)


Out[22]:
0.0 0.2 0.4 0.6 0.8 1.0 0.00000 0.00001 0.00002 EEM fig 1.1 u(x) x [m] Deflection [m]

In [23]:
plot(p..., layout=(2, 1))


Out[23]:
0.0 0.2 0.4 0.6 0.8 1.0 -6 -5 -4 -3 -2 -1 0 EEM fig 1.1 N(x) x [m] Normal force [N] 0.0 0.2 0.4 0.6 0.8 1.0 0.00000 0.00001 0.00002 EEM fig 1.1 u(x) x [m] Deflection [m]

Step by step through FE4_1

Above the results using FE4_1.jl. Below goes through the steps in FE4_1.jl. Initial part of FE4_1 just checks the input data Dictionary.


In [24]:
if :struc_el in keys(data)
    struc_el = data[:struc_el]
end


Out[24]:
CSoM.Rod(9,1,1,CSoM.Line(2,1))

In [25]:
ndim = 1
nst = struc_el.np_types;

In [26]:
fin_el = struc_el.fin_el


Out[26]:
CSoM.Line(2,1)

In [27]:
if typeof(fin_el) == Line # 1D finite element
    (nels, nn) = CSoM.mesh_size(fin_el, struc_el.nxe)
    elseif typeof(fin_el) == Triangle || typeof(fin_el) == Quadrilateral # 2D finite elements
    (nels, nn) = mesh_size(fin_el, struc_el.nxe, struc_el.nye)
    elseif typeof(fin_el) == Hexahedron  # 3D finite elements
    (nels, nn) = mesh_size(fin_el, struc_el.nxe, struc_el.nye, struc_el.nze)
end


Out[27]:
(9,10)

In [28]:
nodof = fin_el.nodof         # Degrees of freedom per node


Out[28]:
1

In [29]:
ndof = fin_el.nod * nodof    # Degrees of freedom per fin_el, in this case each finite element has 2 nodes


Out[29]:
2

In [30]:
penalty = 1e20                         # used to fix fixed_freedoms
if :penalty in keys(data)
    penalty = data[:penalty]
end

In [31]:
if :properties in keys(data)
    prop = zeros(size(data[:properties], 1), size(data[:properties], 2))
    for i in 1:size(data[:properties], 1)
        prop[i, :] = data[:properties][i, :]
    end
end

In [32]:
prop


Out[32]:
1×1 Array{Float64,2}:
 100000.0

In [33]:
nf = ones(Int64, nodof, nn)
if :support in keys(data)
    for i in 1:size(data[:support], 1)
        nf[:, data[:support][i][1]] = data[:support][i][2]
    end
end
(size(nf), nf)


Out[33]:
((1,10),
[1 1 … 1 0])

In [34]:
x_coords = zeros(nn)
if :x_coords in keys(data)
    x_coords = data[:x_coords]
end
  
y_coords = zeros(nn)
if :y_coords in keys(data)
    y_coords = data[:y_coords]
end
  
z_coords = zeros(nn)
if :z_coords in keys(data)
    z_coords = data[:z_coords]
end

etype = ones(Int64, nels)
if :etype in keys(data)
    etype = data[:etype]
end
x_coords


Out[34]:
0.0:0.1111111111111111:1.0

In [35]:
collect(x_coords)


Out[35]:
10-element Array{Float64,1}:
 0.0     
 0.111111
 0.222222
 0.333333
 0.444444
 0.555556
 0.666667
 0.777778
 0.888889
 1.0     

In [36]:
#
# Initialize dynamic arrays stored in the FEM object
#

points = zeros(struc_el.nip, ndim)
g = zeros(Int64, ndof)
g_coord = zeros(ndim,nn)
fun = zeros(fin_el.nod)
coord = zeros(fin_el.nod, ndim)
gamma = zeros(nels)
jac = zeros(ndim, ndim)
g_num = zeros(Int64, fin_el.nod, nels)
der = zeros(ndim, fin_el.nod)
deriv = zeros(ndim, fin_el.nod)
bee = zeros(nst,ndof)
km = zeros(ndof, ndof)
mm = zeros(ndof, ndof)
gm = zeros(ndof, ndof)
kg = zeros(ndof, ndof)
eld = zeros(ndof)
weights = zeros(struc_el.nip)
g_g = zeros(Int64, ndof, nels)
num = zeros(Int64, fin_el.nod)
actions = zeros(nels, ndof)
displacements = zeros(size(nf, 1), ndim)
gc = ones(ndim, ndim)
dee = zeros(nst,nst)
sigma = zeros(nst)
axial = zeros(nels);

Ok, most arrays have been initialized. Time to start the real work. First determine the global numbering:


In [37]:
?CSoM.formnf!


Out[37]:

formnf!

Returns nodal freedom numbering array nf

Function

formnf!(nodof::Int64, nn::Int64, nf::Matrix{Int64})

Arguments

* nodof::Int64       : Number of degrees of freedom for each node
* nn::Int64          : Number of nodes in mesh
* nf::Array{Int64,2} : Nodal freedom matrix (updated)

In [38]:
CSoM.formnf!(nodof, nn, nf)
nf


Out[38]:
1×10 Array{Int64,2}:
 1  2  3  4  5  6  7  8  9  0

Node N (= 10 in this example) is fixed, nodes 1 to 9 represents the degrees of freedom. We need 9 equations to solve for these 9 displacements.


In [39]:
neq = maximum(nf)


Out[39]:
9

In [40]:
kdiag = zeros(Int64, neq);

In [41]:
ell = zeros(nels) # Used to hold element length
if :x_coords in keys(data)
    for i in 1:length(data[:x_coords])-1
        ell[i] = data[:x_coords][i+1] - data[:x_coords][i]
    end
end
ell


Out[41]:
9-element Array{Float64,1}:
 0.111111
 0.111111
 0.111111
 0.111111
 0.111111
 0.111111
 0.111111
 0.111111
 0.111111

In [42]:
for i in 1:nels
    num = [i; i+1]
    CSoM.num_to_g!(fin_el.nod, nodof, nn, ndof, num, nf, g)
    g_g[:, i] = g
    CSoM.fkdiag!(ndof, neq, g, kdiag)
end

Matrix g_g now holds the globally numbered degrees of freedom for the Line elements. Note that the 2nd node of element 9 is fixed.


In [43]:
g_g


Out[43]:
2×9 Array{Int64,2}:
 1  2  3  4  5  6  7  8  9
 2  3  4  5  6  7  8  9  0

Compute the locations in the skyline vector kv which form the diagonal elements of the full stiffness matrix.


In [44]:
for i in 2:neq
    kdiag[i] = kdiag[i] + kdiag[i-1]
end
kdiag


Out[44]:
9-element Array{Int64,1}:
  1
  3
  5
  7
  9
 11
 13
 15
 17

Above kdiag holds the indices of the diagonal elements of the stiffness matrix in the kv skyline vector.

To illustrate this, we'll borrow kv from the earlier call to FE4_1:


In [45]:
m.kv


Out[45]:
17-element Array{Float64,1}:
  948.683
 -948.683
  948.683
 -948.683
  948.683
 -948.683
  948.683
 -948.683
  948.683
 -948.683
  948.683
 -948.683
  948.683
 -948.683
  948.683
 -948.683
  948.683

The function fromSkyline(m.kv, m.kdiag) uses the indices in kdiag to reconstruct the (symmetrical) stiffness matrix.


In [46]:
sm = fromSkyline(m.kv, m.kdiag)


Out[46]:
9×9 Array{Float64,2}:
  948.683  -948.683     0.0       0.0    …     0.0       0.0       0.0  
 -948.683   948.683  -948.683     0.0          0.0       0.0       0.0  
    0.0    -948.683   948.683  -948.683        0.0       0.0       0.0  
    0.0       0.0    -948.683   948.683        0.0       0.0       0.0  
    0.0       0.0       0.0    -948.683        0.0       0.0       0.0  
    0.0       0.0       0.0       0.0    …  -948.683     0.0       0.0  
    0.0       0.0       0.0       0.0        948.683  -948.683     0.0  
    0.0       0.0       0.0       0.0       -948.683   948.683  -948.683
    0.0       0.0       0.0       0.0          0.0    -948.683   948.683

Julia has it's own sparse matrix representation. At some point the intenstion is to replace the skyline format with the Julia SparseArrays representation.


In [67]:
?AbstractSparseArray


search: AbstractSparseArray AbstractSparseMatrix AbstractSparseVector

Out[67]:

No documentation found.

Summary:

abstract AbstractSparseArray{Tv,Ti,N} <: AbstractArray{Tv,N}

Subtypes:

Base.SparseArrays.CHOLMOD.Sparse{Tv<:Union{Complex{Float64},Float64}}
SparseMatrixCSC{Tv,Ti<:Integer}
SparseVector{Tv,Ti<:Integer}

In [47]:
sparse(sm)


Out[47]:
9×9 sparse matrix with 25 Float64 nonzero entries:
	[1, 1]  =  948.683
	[2, 1]  =  -948.683
	[1, 2]  =  -948.683
	[2, 2]  =  948.683
	[3, 2]  =  -948.683
	[2, 3]  =  -948.683
	[3, 3]  =  948.683
	[4, 3]  =  -948.683
	[3, 4]  =  -948.683
	[4, 4]  =  948.683
	⋮
	[5, 6]  =  -948.683
	[6, 6]  =  948.683
	[7, 6]  =  -948.683
	[6, 7]  =  -948.683
	[7, 7]  =  948.683
	[8, 7]  =  -948.683
	[7, 8]  =  -948.683
	[8, 8]  =  948.683
	[9, 8]  =  -948.683
	[8, 9]  =  -948.683
	[9, 9]  =  948.683

In [48]:
kv = zeros(kdiag[neq])
gv = zeros(kdiag[neq])
print("There are $(neq) equations,")
println(" and the skyline storage is $(kdiag[neq]).\n")


There are 9 equations, and the skyline storage is 17.


In [49]:
loads = zeros(neq+1)
if :loaded_nodes in keys(data)
    for i in 1:size(data[:loaded_nodes], 1)
        loads[nf[:, data[:loaded_nodes][i][1]]+1] = 
            data[:loaded_nodes][i][2]
    end
end
nf


Out[49]:
1×10 Array{Int64,2}:
 1  2  3  4  5  6  7  8  9  0

In [50]:
for i in 1:nels
    km = CSoM.rod_km!(km, prop[etype[i], 1], ell[i])
    g = g_g[:, i]
    CSoM.fsparv!(kv, km, g, kdiag)
end

In [51]:
km


Out[51]:
2×2 Array{Float64,2}:
  900000.0  -900000.0
 -900000.0   900000.0

In [52]:
kv


Out[52]:
17-element Array{Float64,1}:
  900000.0  
 -900000.0  
       1.8e6
 -900000.0  
       1.8e6
 -900000.0  
       1.8e6
 -900000.0  
       1.8e6
 -900000.0  
       1.8e6
 -900000.0  
       1.8e6
 -900000.0  
       1.8e6
 -900000.0  
       1.8e6

In [53]:
# Add radial stress if fin_el is 3d and axisymmetric
if ndim == 3 && struc_el.axisymmetric
    nst = 4
end

In [54]:
fixed_freedoms = 0
if :fixed_freedoms in keys(data)
    fixed_freedoms = size(data[:fixed_freedoms], 1)
end
no = zeros(Int64, fixed_freedoms)
node = zeros(Int64, fixed_freedoms)
sense = zeros(Int64, fixed_freedoms)
value = zeros(Float64, fixed_freedoms)
if :fixed_freedoms in keys(data) && fixed_freedoms > 0
    for i in 1:fixed_freedoms
      node[i] = data[:fixed_freedoms][i][1]
      sense[i] = data[:fixed_freedoms][i][2]
      no[i] = nf[sense[i], node[i]]
      value[i] = data[:fixed_freedoms][i][3]
    end
    kv[kdiag[no]] = kv[kdiag[no]] + penalty
    loads[no+1] = kv[kdiag[no]] .* value
end
loads


Out[54]:
10-element Array{Float64,1}:
 0.277778
 0.277778
 0.555556
 0.555556
 0.555556
 0.555556
 0.555556
 0.555556
 0.555556
 0.555556

In [55]:
CSoM.sparin!(kv, kdiag)

In [68]:
kv


Out[68]:
17-element Array{Float64,1}:
  948.683
 -948.683
  948.683
 -948.683
  948.683
 -948.683
  948.683
 -948.683
  948.683
 -948.683
  948.683
 -948.683
  948.683
 -948.683
  948.683
 -948.683
  948.683

In [56]:
loads[2:end] = CSoM.spabac!(kv, loads[2:end], kdiag)
loads


Out[56]:
10-element Array{Float64,1}:
 0.277778  
 2.5e-5    
 2.46914e-5
 2.37654e-5
 2.22222e-5
 2.00617e-5
 1.7284e-5 
 1.38889e-5
 9.87654e-6
 5.24691e-6

In [57]:
displacements = zeros(size(nf))
for i in 1:size(displacements, 1)
    for j in 1:size(displacements, 2)
      if nf[i, j] > 0
        displacements[i,j] = loads[nf[i, j]+1]
      end
    end
end
displacements = displacements'


Out[57]:
10×1 Array{Float64,2}:
 2.5e-5    
 2.46914e-5
 2.37654e-5
 2.22222e-5
 2.00617e-5
 1.7284e-5 
 1.38889e-5
 9.87654e-6
 5.24691e-6
 0.0       

In [58]:
loads[1] = 0.0
for i in 1:nels
    km = CSoM.rod_km!(km, prop[etype[i], 1], ell[i])
    g = g_g[:, i]
    eld = loads[g+1]
    actions[i, :] = km * eld
end
actions


Out[58]:
9×2 Array{Float64,2}:
 0.277778  -0.277778
 0.833333  -0.833333
 1.38889   -1.38889 
 1.94444   -1.94444 
 2.5       -2.5     
 3.05556   -3.05556 
 3.61111   -3.61111 
 4.16667   -4.16667 
 4.72222   -4.72222 

In [59]:
m=FEM(struc_el, fin_el, ndim, nels, nst, ndof, nn, nodof, neq, penalty,
    etype, g, g_g, g_num, kdiag, nf, no, node, num, sense, actions, 
    bee, coord, gamma, dee, der, deriv, displacements, eld, fun, gc,
    g_coord, jac, km, mm, gm, kv, gv, loads, points, prop, sigma, value,
    weights, x_coords, y_coords, z_coords, axial);

In [60]:
m.displacements


Out[60]:
10×1 Array{Float64,2}:
 2.5e-5    
 2.46914e-5
 2.37654e-5
 2.22222e-5
 2.00617e-5
 1.7284e-5 
 1.38889e-5
 9.87654e-6
 5.24691e-6
 0.0       

In [61]:
m.actions


Out[61]:
9×2 Array{Float64,2}:
 0.277778  -0.277778
 0.833333  -0.833333
 1.38889   -1.38889 
 1.94444   -1.94444 
 2.5       -2.5     
 3.05556   -3.05556 
 3.61111   -3.61111 
 4.16667   -4.16667 
 4.72222   -4.72222 

In [ ]: