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 Julia 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]