File: demo_elastodynamics.py.rst

package info (click to toggle)
dolfin 2019.2.0~legacy20240219.1c52e83-18
  • links: PTS, VCS
  • area: main
  • in suites: trixie
  • size: 31,700 kB
  • sloc: xml: 104,040; cpp: 102,227; python: 24,356; sh: 460; makefile: 330; javascript: 226
file content (503 lines) | stat: -rw-r--r-- 23,674 bytes parent folder | download | duplicates (5)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503

..    # gedit: set fileencoding=utf8 :

.. _demo_elastodynamics:

============================================
Time-integration of elastodynamics equation
============================================

This demo is implemented in a single Python file,
:download:`demo_elastodynamics.py`, which contains both the
variational forms and the solver.

This demo shows how to perform time integration of transient elastodynamics using the generalized-:math:`\alpha` method [ERL2002]_. In particular it demonstrates how to:

* formulate mass and damping forms of the elastodynamics equation
* implement the generalized-:math:`\alpha` method and its influence on the solution
* perform efficient computation of stresses using ``LocalSolver``

The deformed structure evolution over time along with the axial stress will look as follows:

.. image:: elastodynamics-transient_deformation.gif
   :width: 70%
   :align: center


----------------------------------------- 
Introduction and elastodynamics equation
-----------------------------------------

The elastodynamics equation combine the balance of linear momentum:

.. math::
   \nabla \cdot \sigma + \rho b = \rho \ddot{u}

where :math:`u` is the displacement vector field, :math:`\ddot{u}=\partial^2 u/\partial t^2` is the acceleration, 
:math:`\rho` the material density, :math:`b` a given body force and :math:`\sigma` the stress tensor which is related 
to the displacement through a constitutive equation. In the case of isotropic linearized elasticity, one has:

.. math::
   \sigma =\lambda \text{tr}(\varepsilon)\mathbb{1} + 2\mu\varepsilon

where :math:`\varepsilon = (\nabla u + (\nabla u)^T)/2` is the linearized strain tensor, :math:`\mathbb{1}` is the 
identity of second-rank tensors and :math:`\lambda=\dfrac{E\nu}{(1+\nu)(1-2\nu)},\mu=\dfrac{E}{2(1+\nu)}` are the 
Lame coefficients given as functions of the Young modulus :math:`E` and the Poisson ratio :math:`\nu`.

The weak form is readily obtained by integrating by part the balance equation using a test function :math:`v\in V`
with :math:`V` being a suitable function space that satisfies the displacement boundary conditions:

.. math::
   \int_{\Omega} \rho \ddot{u}\cdot v \, {\rm d} x + \int_{\Omega} \sigma(u):\varepsilon(v) \, {\rm d} x = 
   \int_{\Omega} \rho b \cdot v  \, {\rm d} x + \int_{\partial\Omega} (\sigma\cdot n) \cdot v \, {\rm d} s \quad \text{for all } v\in V

The previous equation can be written as follows:

.. math::
   \text{Find }u\in V\text{ such that } m(\ddot{u},v) + k(u,v) = L(v) \quad \text{for all } v\in V

where :math:`m` is the symmetric bilinear form associated with the mass matrix and :math:`k` the one associated with the stiffness matrix.

After introducing the finite element space interpolation, one obtains the corresponding discretized evolution equation:

.. math::
   \text{Find }\{u\}\in\mathbb{R}^n\text{ such that } \{v\}^T[M]\{\ddot{u}\} + \{v\}^T[K]\{u\} = \{v\}^T\{F\} \quad \text{for all } \{v\}\in\mathbb{R}^n

which is a generalized :math:`n`-dof harmonic oscillator equation.

Quite often in structural dynamics, structures do not oscillate perfectly but lose energy through various dissipative mechanisms (friction with air or supports,
internal dissipation through plasticity, damage, etc.). Dissipative terms can be introduced at the level of the constitutive equation if these mechanisms are well
known but quite often it is not the case. Dissipation can then be modeled by adding an *ad hoc* damping term depending on the structure velocity :math:`\dot{u}` 
to the previous evolution equation:

.. math::
   \text{Find }u\in V\text{ such that } m(\ddot{u},v) + c(\dot{u},v) + k(u,v) = L(v) \quad \text{for all } v\in V

The damping form will be considered here as bilinear and symmetric, being therefore associated with a damping matrix :math:`[C]`.

~~~~~~~~~~~~~~~~~
Rayleigh damping
~~~~~~~~~~~~~~~~~

When little is known about the origin of damping in the structure, a popular choice for the damping matrix, known as *Rayleigh damping*, consists in using
a linear combination of the mass and stiffness matrix :math:`[C] = \eta_M[M]+\eta_K[K]` with two positive parameters :math:`\eta_M,\eta_K` which 
can be fitted against experimental measures for instance (usually by measuring the damping ratio of two natural modes of vibration).

---------------------------------------------------------------
Time discretization using the generalized-:math:`\alpha` method
---------------------------------------------------------------

We now introduce a time discretization of the interval study :math:`[0;T]` in :math:`N+1` time increments :math:`t_0=0,t_1,\ldots,t_N,t_{N+1}=T` 
with :math:`\Delta t=T/N` denoting the time step (supposed constant). The resolution will make use of the generalized-:math:`\alpha` method
which can be seen as an extension of the widely used Newmark-:math:`\beta` method in structural dynamics. As an implicit method, it is unconditionally 
stable for a proper choice of coefficients so that quite large time steps can be used. It also allows for high frequency dissipation and offers a 
second-order accuracy, i.e. in :math:`O(\Delta t^2)`.

The method consists in solving the dynamic evolution equation at intermediate time between :math:`t_n` and :math:`t_{n+1}` as follows:

.. math::
   [M]\{\ddot{u}_{n+1-\alpha_m}\} + [C]\{\dot{u}_{n+1-\alpha_f}\}+[K]\{u_{n+1-\alpha_f}\} = \{F(t_{n+1-\alpha_f})\}

with the notation :math:`X_{n+1-\alpha} = (1-\alpha)X_{n+1}+\alpha X_{n}`. In addition, the following approximation for the displacement and velocity
at :math:`t_{n+1}` are used: 

.. math::
   \begin{align*}
   \{u_{n+1}\} &= \{u_{n}\}+\Delta t \{\dot{u}_{n}\} + \dfrac{\Delta t^2}{2}\left((1-2\beta)\{\ddot{u}_{n}\}+2\beta\{\ddot{u}_{n+1}\}\right) \\
   \{\dot{u}_{n+1}\} &= \{\dot{u}_{n}\} + \Delta t\left((1-\gamma)\{\ddot{u}_{n}\}+\gamma\{\ddot{u}_{n+1}\}\right)
   \end{align*}

It can be seen that these are the relations of the Newmark method. The latter is therefore obtained as a particular case when :math:`\alpha_f=\alpha_m=0`.

The problem can then be formulated in terms of unkown displacement at :math:`t_{n+1}` with:

.. math::
   \{\ddot{u}_{n+1}\} = \dfrac{1}{\beta\Delta t^2}\left(\{u_{n+1}\} - \{u_{n}\}-\Delta t \{\dot{u}_{n}\} \right) - \dfrac{1-2\beta}{2\beta}\{\ddot{u}_{n}\}

After plugging into the evolution and rearranging the known and unknown terms, one obtains the following system to solve:

.. math::
   \begin{align*}
   [\bar{K}]\{u_{n+1}\} &= \{F(t_{n+1-\alpha_f})\} - \alpha_f[K]\{u_n\} \\
	&- [C](c_1\{u_n\}+c_2\{\dot{u}_n\}+c_3\{\ddot{u}_n\})-[M](m_1\{u_n\}+m_2\{\dot{u}_n\}+m_3\{\ddot{u}_n\})
 \end{align*}

where:
 * :math:`[\bar{K}] = [K]+c_1[C]+m_1[M]`
 * :math:`c_1 = \dfrac{\gamma(1-\alpha_f)}{\beta\Delta t}`
 * :math:`c_2 = 1-\gamma(1-\alpha_f)/\beta`
 * :math:`c_3 = \Delta t(1-\alpha_f)(1-\dfrac{\gamma}{2\beta})`
 * :math:`m_1 = \dfrac{(1-\alpha_m)}{\beta\Delta t^2}`
 * :math:`m_2 = \dfrac{(1-\alpha_m)}{\beta\Delta t}`
 * :math:`m_3 = 1-\dfrac{1-\alpha_m}{2\beta}`
 
Once the linear system has been solved for :math:`\{u_{n+1}\}`, the new velocity and acceleration are computed using the previous formulae.

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Popular choice of parameters
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The most popular choice for the parameters is: :math:`\alpha_m,\alpha_f \leq 1/2` and :math:`\gamma=\dfrac{1}{2}+\alpha_m-\alpha_f`,
:math:`\beta=\dfrac{1}{4}\left(\gamma+\dfrac{1}{2}\right)^2` which ensures unconditional stability, optimal dissipation and second-order accuracy.


---------------
Implementation
---------------

We consider a rectangular beam clamped at one end and loaded by a uniform vertical traction at the other end.
After importing the relevant modules, the mesh and subdomains for boundary conditions are defined::

 from dolfin import *
 import numpy as np
 import matplotlib.pyplot as plt

 # Form compiler options
 parameters["form_compiler"]["cpp_optimize"] = True
 parameters["form_compiler"]["optimize"] = True

 # Define mesh
 mesh = BoxMesh(Point(0., 0., 0.), Point(1., 0.1, 0.04), 60, 10, 5)

 # Sub domain for clamp at left end
 def left(x, on_boundary):
     return near(x[0], 0.) and on_boundary

 # Sub domain for rotation at right end
 def right(x, on_boundary):
     return near(x[0], 1.) and on_boundary


Material parameters for the elastic constitutive relation, the material density :math:`\rho`
for the mass matrix and the two parameters defining the Rayleigh damping :math:`\eta_M,\eta_K`
(initially zero damping is considered but this value can be changed) are now defined::

 # Elastic parameters
 E  = 1000.0
 nu = 0.3
 mu    = Constant(E / (2.0*(1.0 + nu)))
 lmbda = Constant(E*nu / ((1.0 + nu)*(1.0 - 2.0*nu)))

 # Mass density
 rho = Constant(1.0)

 # Rayleigh damping coefficients
 eta_m = Constant(0.)
 eta_k = Constant(0.)

Parameters used for the time discretization scheme are now defined. First, the four parameters used by the
generalized-:math:`\alpha` method are chosen. Here, we used the optimal dissipation and second-order accuracy
choice for :math:`\beta` and :math:`\gamma`, namely :math:`\beta=\dfrac{1}{4}\left(\gamma+\dfrac{1}{2}\right)^2` and
:math:`\gamma=\dfrac{1}{2}+\alpha_m-\alpha_f` with :math:`\alpha_m=0.2` and :math:`\alpha_f=0.4` ensuring unconditional stability::

 # Generalized-alpha method parameters
 alpha_m = Constant(0.2)
 alpha_f = Constant(0.4)
 gamma   = Constant(0.5+alpha_f-alpha_m)
 beta    = Constant((gamma+0.5)**2/4.)

We also define the final time of the interval, the number of time steps and compute the associated time interval
between two steps::

 # Time-stepping parameters
 T       = 4.0
 Nsteps  = 50
 dt = Constant(T/Nsteps)


We now define the time-dependent loading. Body forces are zero and the imposed loading consists of a uniform vertical traction
applied at the ``right`` extremity. The loading amplitude will vary linearly from :math:`0` to :math:`p_0=1` over the time interval
:math:`[0;T_c=T/5]`, after :math:`T_c` the loading is removed. For this purpose, we used the following JIT-compiled ``Expression``.
In particular, it uses a conditional syntax using operators ``?`` and ``:`` ::

 p0 = 1.
 cutoff_Tc = T/5
 # Define the loading as an expression depending on t
 p = Expression(("0", "t <= tc ? p0*t/tc : 0", "0"), t=0, tc=cutoff_Tc, p0=p0, degree=0)

A standard vectorial :math:`P^1` FunctionSpace is now defined for the displacement, velocity and acceleration fields. We also
define a tensorial DG-0 FunctionSpace for saving the stress field evolution::

 # Define function space for displacement, velocity and acceleration
 V = VectorFunctionSpace(mesh, "CG", 1)
 # Define function space for stresses
 Vsig = TensorFunctionSpace(mesh, "DG", 0)

Test and trial functions are defined and the unkown displacement (corresponding to :math:`\{u_{n+1}\}` for the current time step)
will be represented by the Function ``u``. Displacement, velocity and acceleration fields of the previous increment
:math:`t_n` will respectively be represented by functions ``u_old``, ``v_old`` and ``a_old``::

 # Test and trial functions
 du = TrialFunction(V)
 u_ = TestFunction(V)
 # Current (unknown) displacement
 u = Function(V, name="Displacement")
 # Fields from previous time step (displacement, velocity, acceleration)
 u_old = Function(V)
 v_old = Function(V)
 a_old = Function(V)

We now use a ``MeshFunction`` for distinguishing the different boundaries and mark the right extremity using an ``AutoSubDomain``.
The exterior surface measure ``ds`` is then defined using the boundary subdomains. Simple Dirichlet boundary conditions are also defined at the left extremity::

 # Create mesh function over the cell facets
 boundary_subdomains = MeshFunction("size_t", mesh, mesh.topology().dim() - 1)
 boundary_subdomains.set_all(0)
 force_boundary = AutoSubDomain(right)
 force_boundary.mark(boundary_subdomains, 3)

 # Define measure for boundary condition integral
 dss = ds(subdomain_data=boundary_subdomains)

 # Set up boundary condition at left end
 zero = Constant((0.0, 0.0, 0.0))
 bc = DirichletBC(V, zero, left)

Python functions are now defined to obtain the elastic stress tensor :math:`\sigma` (linear isotropic elasticity), the bilinear mass and stiffness forms as well
as the damping form obtained as a linear combination of the mass and stiffness forms (Rayleigh damping). The linear form corresponding to the work of external forces is also defined::

 # Stress tensor
 def sigma(r):
     return 2.0*mu*sym(grad(r)) + lmbda*tr(sym(grad(r)))*Identity(len(r))

 # Mass form
 def m(u, u_):
     return rho*inner(u, u_)*dx

 # Elastic stiffness form
 def k(u, u_):
     return inner(sigma(u), sym(grad(u_)))*dx

 # Rayleigh damping form
 def c(u, u_):
     return eta_m*m(u, u_) + eta_k*k(u, u_)

 # Work of external forces
 def Wext(u_):
     return dot(u_, p)*dss(3)

Functions for implementing the time stepping scheme are also defined. ``update_a`` returns :math:`\{\ddot{u}_{n+1}\}` 
as a function of the variables at the previous increment and of the new displacement :math:`\{u_{n+1}\}`. The function accepts a keyword ``ufl`` so that the expressions involved can be used with UFL representations if ``True`` or with array of values if ``False`` (we will make use of both possibilities later). 
In particular, the time step ``dt`` and time-stepping scheme parameters are either ``Constant`` or floats depending on the case.
Function ``update_v`` does the same but for the new velocity :math:`\{\dot{u}_{n+1}\}` as a function of the previous variables 
and of the new acceleration. Finally, function ``update_fields`` performs the final update at the end of the time step when the new 
displacement :math:`\{u_{n+1}\}` has effectively been computed. In this context, the new acceleration and velocities are computed 
using the vector representation of the different fields. The variables keeping track of the values at the previous increment are now assigned the new values computed for the current increment::

 # Update formula for acceleration
 # a = 1/(2*beta)*((u - u0 - v0*dt)/(0.5*dt*dt) - (1-2*beta)*a0)
 def update_a(u, u_old, v_old, a_old, ufl=True):
     if ufl:
         dt_ = dt
         beta_ = beta
     else:
         dt_ = float(dt)
         beta_ = float(beta)
     return (u-u_old-dt_*v_old)/beta_/dt_**2 - (1-2*beta_)/2/beta_*a_old

 # Update formula for velocity
 # v = dt * ((1-gamma)*a0 + gamma*a) + v0
 def update_v(a, u_old, v_old, a_old, ufl=True):
     if ufl:
         dt_ = dt
         gamma_ = gamma
     else:
         dt_ = float(dt)
         gamma_ = float(gamma)
     return v_old + dt_*((1-gamma_)*a_old + gamma_*a)

 def update_fields(u, u_old, v_old, a_old):
     """Update fields at the end of each time step.""" 

     # Get vectors (references)
     u_vec, u0_vec  = u.vector(), u_old.vector()
     v0_vec, a0_vec = v_old.vector(), a_old.vector() 
 
     # use update functions using vector arguments
     a_vec = update_a(u_vec, u0_vec, v0_vec, a0_vec, ufl=False)
     v_vec = update_v(a_vec, u0_vec, v0_vec, a0_vec, ufl=False)
 
     # Update (u_old <- u)
     v_old.vector()[:], a_old.vector()[:] = v_vec, a_vec
     u_old.vector()[:] = u.vector()

The system variational form is now built by expressing the new acceleration :math:`\{\ddot{u}_{n+1}\}` as a function of
the TrialFunction ``du`` using ``update_a``, which here works as a UFL expression. Using this new acceleration, the same is
done for the new velocity using ``update_v``. Intermediate averages using parameters :math:`\alpha_m,\alpha_f` of the generalized- :math:`\alpha`
method are obtained with a user-defined fuction ``avg``. The weak form evolution equation is then written using all these 
quantities. Since the problem is linear, we then extract the bilinear and linear parts using ``rhs`` and ``lhs``::

 def avg(x_old, x_new, alpha):
     return alpha*x_old + (1-alpha)*x_new

 # Residual
 a_new = update_a(du, u_old, v_old, a_old, ufl=True)
 v_new = update_v(a_new, u_old, v_old, a_old, ufl=True)
 res = m(avg(a_old, a_new, alpha_m), u_) + c(avg(v_old, v_new, alpha_f), u_) \
        + k(avg(u_old, du, alpha_f), u_) - Wext(u_)
 a_form = lhs(res)
 L_form = rhs(res)

Alternatively, the use of ``derivative`` can be made for non-linear problems for instance or one can also directly 
formulate the system to solve, involving the modified stiffness matrix :math:`[\bar{K}]` and the various coefficients introduced earlier.

Since the system matrix to solve is the same for each time step (constant time step), it is not necessary to factorize the system at each increment.
It can be done once and for all and only perform assembly of the varying right-hand side and backsubstitution to obtain the solution 
much more efficiently. This is done by defining a ``LUSolver`` object while PETSc handles caching factorizations::

 # Define solver for reusing factorization
 K, res = assemble_system(a_form, L_form, bc)
 solver = LUSolver(K, "mumps")
 solver.parameters["symmetric"] = True

We now initiate the time stepping loop. We will keep track of the beam vertical tip displacement over time as well as the different
parts of the system total energy. We will also compute the stress field and save it, along with the displacement field, in a ``XDMFFile``.  
The option `flush_ouput` enables to open the result file before the loop is finished, the ``function_share_mesh`` option tells that only one
mesh is used for all functions of a given time step (displacement and stress) while the ``rewrite_function_mesh`` enforces that the same mesh
is used for all time steps. These two options enables writing the mesh information only once instead of :math:`2N_{steps}` times::

 # Time-stepping
 time = np.linspace(0, T, Nsteps+1)
 u_tip = np.zeros((Nsteps+1,))
 energies = np.zeros((Nsteps+1, 4))
 E_damp = 0
 E_ext = 0
 sig = Function(Vsig, name="sigma")
 xdmf_file = XDMFFile("elastodynamics-results.xdmf")
 xdmf_file.parameters["flush_output"] = True
 xdmf_file.parameters["functions_share_mesh"] = True
 xdmf_file.parameters["rewrite_function_mesh"] = False

The time loop is now started, the loading is first evaluated at :math:`t=t_{n+1-\alpha_f}`. The corresponding system right-hand side is then
assembled and the system is solved. The different fields are then updated with the newly computed quantities. Finally, some post-processing is
performed: stresses are computed and written to the result file and the tip displacement and the different energies are recorded::

 def local_project(v, V, u=None):
     """Element-wise projection using LocalSolver"""
     dv = TrialFunction(V)
     v_ = TestFunction(V)
     a_proj = inner(dv, v_)*dx
     b_proj = inner(v, v_)*dx
     solver = LocalSolver(a_proj, b_proj)
     solver.factorize()
     if u is None:
         u = Function(V)
         solver.solve_local_rhs(u)
         return u
     else:
         solver.solve_local_rhs(u)
         return

 for (i, dt) in enumerate(np.diff(time)):
 
     t = time[i+1]
     print("Time: ", t)

     # Forces are evaluated at t_{n+1-alpha_f}=t_{n+1}-alpha_f*dt
     p.t = t-float(alpha_f*dt)

     # Solve for new displacement
     res = assemble(L_form)
     bc.apply(res)
     solver.solve(K, u.vector(), res)


     # Update old fields with new quantities
     update_fields(u, u_old, v_old, a_old)

     # Save solution to XDMF format
     xdmf_file.write(u, t)

     # Compute stresses and save to file
     local_project(sigma(u), Vsig, sig)
     xdmf_file.write(sig, t)

     p.t = t
     # Record tip displacement and compute energies
     # Note: Only works in serial
     if MPI.comm_world.size == 1:
         u_tip[i+1] = u(1., 0.05, 0.)[1]
     E_elas = assemble(0.5*k(u_old, u_old))
     E_kin = assemble(0.5*m(v_old, v_old))
     E_damp += dt*assemble(c(v_old, v_old))
     # E_ext += assemble(Wext(u-u_old))
     E_tot = E_elas+E_kin+E_damp #-E_ext
     energies[i+1, :] = np.array([E_elas, E_kin, E_damp, E_tot])

Note that in the above, the stresses are computed using a ``LocalSolver`` through the ``local_project`` function. Since the stress function space
is a DG-0 space, the projection on this space can be performed element-wise in a very efficient manner. We therefore take advantage of the ``LocalSolver`` 
functionality which is precisely dedicated to such situations. Since this projection is performed at each time step, the savings in terms of computing
time can be quite important.

As regards the computation of the various energies, the elastic and kinetic energies are respectively given by:

.. math::
   E_{elas} = \int_{\Omega} \dfrac{1}{2}\sigma(u):\varepsilon(u) \, {\rm d} x
.. math::
   E_{kin} = \int_{\Omega} \dfrac{1}{2}\rho \dot{u}\cdot\dot{u} \, {\rm d} x

which are readily computed from the respective stiffness and mass forms ``k`` and ``m`` and the current displacement and velocity. The energy related to damping
is computed from the corresponding dissipation term :math:`\mathcal{D}=c(\dot{u},\dot{u})` and integrated over time:

.. math::
   E_{damp} = \int_0^T \mathcal{D} \, {\rm d} t

As for the work developed by the external forces, the contribution to the energy is added at each time step. Finally, the total energy of the sytem is given by:

.. math::
   E_{tot} = E_{elas}+E_{kin}+E_{damp}-E_{ext}

When the time evolution loop is finished, the evolution of the tip displacement as well as the different contributions of the energy are plotted as functions of time::

 if MPI.comm_world.size == 1:
     # Plot tip displacement evolution
     plt.figure()
     plt.plot(time, u_tip)
     plt.xlabel("Time")
     plt.ylabel("Tip displacement")
     plt.ylim(-0.5, 0.5)
     plt.show()

 if (MPI.comm_world.rank == 0):
     # Plot energies evolution
     plt.figure()
     plt.plot(time, energies)
     plt.legend(("elastic", "kinetic", "damping", "total"))
     plt.xlabel("Time")
     plt.ylabel("Energies")
     plt.ylim(0, 0.0011)
     plt.show()

---------------------
Analyzing the results
---------------------

We first consider the case of zero Rayleigh damping :math:`\eta_M=\eta_K=0`. In this case, it can be observed that the evolution of the total energy depends
on the choice of the time-stepping scheme parameters. With :math:`\alpha_m=\alpha_f=0`, we recover the Newmark-:math:`\beta` method with :math:`\beta=0.25,\gamma=0.5`.
This scheme is known for being conservative. This can be observed (figure-left) in the constant total energy for :math:`t\geq T_c` when the loading is removed. On the contrary,
for non zero alpha parameters, e.g. :math:`\alpha_m=0.2, \alpha_f=0.4`, it can be observed (figure-right) that the energy is decreasing during this phase, indicating numerical damping.
For both cases, the scheme is unconditionally stable. Moreover, these differences vanish when reducing the time step.

.. figure:: elastodynamics-energies_newmark.png
   :width: 60%
   :align: center

   Newmark-:math:`\beta` method :math:`\alpha_m=\alpha_f=0`

.. figure:: elastodynamics-energies_generalized_alpha.png
   :width: 60%
   :align: center

   Generalized-:math:`\alpha` with :math:`\alpha_m=0.2, \alpha_f=0.4`

For non-zero Rayleigh damping :math:`\eta_M=\eta_K=0.01`, the total energy including viscous dissipation tends to oscillate around a constant value, with oscillations vanishing for decreasing time steps.

.. image:: elastodynamics-energies_generalized_alpha_damping.png
   :width: 60%
   :align: center

-----------
References
-----------

.. [ERL2002] Silvano Erlicher, Luca Bonaventura, Oreste Bursi. The analysis of the Generalized-alpha method for non-linear dynamic problems. Computational Mechanics, Springer Verlag, 2002, 28, pp.83-104, doi:10.1007/s00466-001-0273-z