Self-interaction of a P wave in a 1D nonlinear elastic medium

  • Time-domain, explicit scheme, Spectral elements

  • 1D

  • Murnaghan hyperelasticity

  • P-wave harmonics generation

[1]:
import numpy as np

from dolfinx import mesh, fem, default_scalar_type
import ufl
from mpi4py import MPI
from petsc4py import PETSc

from elastodynamicsx.pde import material, boundarycondition, PDE, PDECONFIG
from elastodynamicsx.solvers import TimeStepper
from elastodynamicsx.plot import plotter
from elastodynamicsx.utils import spectral_element, spectral_quadrature, make_facet_tags, ParallelEvaluator

Set up a Spectral Element Method

[2]:
degElement, nameElement = 4, "GLL"
PDECONFIG.default_metadata = spectral_quadrature(nameElement, degElement)

cell_type = mesh.CellType.interval
specFE = spectral_element(nameElement, cell_type, degElement, (2,))

FE domain

[3]:
length = 10
Nx     = 100 // degElement  # Nb of elts

# create the mesh
extent = [0, length]
domain = mesh.create_interval(MPI.COMM_WORLD, Nx, extent)

# create the function space
V = fem.functionspace(domain, specFE)

# define some tags
tag_left, tag_right = 1, 2
all_tags = (tag_left, tag_right)
boundaries = [(tag_left , lambda x: np.isclose(x[0], 0     )),
              (tag_right, lambda x: np.isclose(x[0], length))]

facet_tags = make_facet_tags(domain, boundaries)

Define the material law

Murnaghan’s hyperelasticity. Parameters: - \(\rho\): density - \(\lambda\), \(\mu\): Lamé’s constants - \(l\), \(m\), \(n\): Murnaghan’s constants

[4]:
# parameters here...
rho     = fem.Constant(domain, default_scalar_type(1))
mu      = fem.Constant(domain, default_scalar_type(1))
lambda_ = fem.Constant(domain, default_scalar_type(2))
l_      = fem.Constant(domain, default_scalar_type(1))
m_      = fem.Constant(domain, default_scalar_type(1))
n_      = fem.Constant(domain, default_scalar_type(1))
# ... end

mat = material(V, 'murnaghan', rho, lambda_, mu, l_, m_, n_)

Boundary conditions

  • Impedance boundary condition on either edge

  • Traction load \(T_N\) on the left edge, with compressive \(_x\) and shear \(_y\) components

[5]:
T_N = fem.Constant(domain, default_scalar_type([0, 0]))  # normal traction (Neumann boundary condition)
Z_N, Z_T = mat.Z_N, mat.Z_T  # P and S mechanical impedances
bc_l  = boundarycondition((V, facet_tags, tag_left), 'Neumann', T_N)
bc_rl = boundarycondition((V, facet_tags, (tag_left, tag_right)), 'Dashpot', Z_N, Z_T)

bcs = [bc_l, bc_rl]

Define the temporal behavior of the source

[6]:
# -> Time function
f0 = 1       # central frequency of the source
T0 = 1 / f0  # period
d0 = 5 * T0  # duration of source


def src_t(t):  # source(t): Sine x Hann window
    window = np.sin(np.pi * t / d0)**2 * (t < d0) * (t > 0)  # Hann window
    return np.sin(2 * np.pi * f0 * t) * window


# -> Space-Time function
p_0 = default_scalar_type(7e-2)  # max amplitude
F_0 = p_0 * default_scalar_type([1, 1])  # source orientation


def T_N_function(t):
    return src_t(t) * F_0


if domain.comm.rank == 0:
    import matplotlib.pyplot as plt
    t = np.linspace(0, 1.1 * d0)
    plt.plot(t, src_t(t) * F_0[0], label=r'$T_{N,x}$: comp.')
    plt.plot(t, src_t(t) * F_0[1], ls='--', label=r'$T_{N,y}$: shear')
    plt.legend()
    plt.xlabel('Time')
    plt.ylabel('Applied force')
    plt.show()
../../_images/demos__ln_demo_weqnl_1D-PSV_Murnaghan_Pwave_11_0.png

Assemble the PDE

[7]:
pde = PDE(V, materials=[mat], bodyforces=[], bcs=bcs)

Time scheme

Leapfrog

[8]:
# Temporal parameters
tstart = 0  # Start time
tmax   = 15 * T0  # Final time
num_steps = 1500
dt = (tmax - tstart) / num_steps  # time step size

# Some control numbers...
hx = length / Nx
c_S = np.sqrt(mu.value / rho.value)  # S-wave velocity
lbda0 = c_S / f0
courant_number = TimeStepper.Courant_number(V.mesh, ufl.sqrt((lambda_ + 2 * mu) / rho), dt)
PETSc.Sys.Print(f'Number of points per wavelength at central frequency: {lbda0 / hx:.2f}')
PETSc.Sys.Print(f'Number of time steps per period at central frequency: {T0 / dt:.2f}')
PETSc.Sys.Print(f'CFL condition: Courant number = {courant_number:.2f}')

# Time integration
#     diagonal=True assumes the left hand side operator is indeed diagonal
tStepper = TimeStepper.build(V,
                             pde.M_fn, pde.C_fn, pde.K_fn, pde.b_fn, dt, bcs=bcs,
                             scheme='leapfrog', diagonal=True)

# Set the initial values
tStepper.set_initial_condition(u0=[0, 0], v0=[0, 0], t0=tstart)
Number of points per wavelength at central frequency: 2.50
Number of time steps per period at central frequency: 100.00
CFL condition: Courant number = 0.05

Define outputs

  • Extract signals at few points

  • Live-plot results (only in a terminal; not in a notebook)

[9]:
u_res = tStepper.timescheme.u  # The solution

# -> Extract signals at few points
# Define points
points_out = np.array([[3, 0, 0], [6, 0, 0], [9, 0, 0]]).T  # shape = (3, nbpts)

# Declare a convenience ParallelEvaluator
paraEval = ParallelEvaluator(domain, points_out)

# Declare data (local)
signals_local = np.zeros((paraEval.nb_points_local,
                          V.num_sub_spaces,
                          num_steps))  # <- output stored here


# -> Define callbacks: will be called at the end of each iteration
def cbck_storeAtPoints(i, out):
    if paraEval.nb_points_local > 0:
        signals_local[:, :, i+1] = u_res.eval(paraEval.points_local, paraEval.cells_local)


# enable live plotting
enable_plot = True
clim = 0.1 * np.amax(F_0) * np.array([0, 1])
kwplot = {'clim': clim, 'warp_factor': 0.5 / np.amax(clim)}
if domain.comm.rank == 0 and enable_plot:
    p = plotter(u_res, refresh_step=10, window_size=[640, 480], **kwplot)
    if paraEval.nb_points_local > 0:
        # add points to live_plotter
        p.add_points(paraEval.points_local, render_points_as_spheres=True, point_size=12)
else:
    p = None

Solve

  • Define a ‘callfirst’ function to update the load

  • Run the time loop

[10]:
# 'callfirsts': will be called at the beginning of each iteration
def cfst_updateSources(t):
    T_N.value = T_N_function(t)


# Run the big time loop!
tStepper.solve(num_steps - 1,
               callfirsts=[cfst_updateSources],
               callbacks=[cbck_storeAtPoints],
               live_plotter=p)
# End of big calc.

Post-processing

  • Plot signals at few points

[11]:
# Gather the data to the root process
all_signals = paraEval.gather(signals_local, root=0)

if domain.comm.rank == 0:
    # -> Exact (linear) solution, At few points
    x = points_out.T
    t = dt * np.arange(num_steps)
    cL = np.sqrt((lambda_.value + 2 * mu.value) / rho.value)
    cS = np.sqrt(mu.value / rho.value)
    resp_L = np.cumsum(src_t(t[np.newaxis, :] - x[:, 0, np.newaxis] / cL) * F_0[0] / 2 / cL / rho.value, axis=1) * dt
    resp_S = np.cumsum(src_t(t[np.newaxis, :] - x[:, 0, np.newaxis] / cS) * F_0[1] / 2 / cS / rho.value, axis=1) * dt
    signals_linear = np.stack((resp_L, resp_S), axis=1)

    f = np.fft.rfftfreq(t.size) / dt

    fig, ax = plt.subplots(V.num_sub_spaces, 2, sharex='col', sharey='none')
    ax[0, 0].set_title('Signals at few points')
    ax[0, 1].set_title('Spectra at few points')
    for icomp, cax in enumerate(ax):
        for i in range(len(all_signals)):
            cax[0].text(0.02, 0.97, 'U' + ['x', 'y', 'z'][icomp], ha='left', va='top', transform=cax[0].transAxes)
            cax[0].plot(t, all_signals[i, icomp, :], c=f'C{i}', ls='-')  # FEM
            cax[0].plot(t, signals_linear[i, icomp, :], c=f'C{i}', ls='--')  # exact linear
            #
            cax[1].plot(f, np.abs(np.fft.rfft(all_signals[i, icomp, :])), c=f'C{i}', ls='-')  # FEM
            cax[1].plot(f, np.abs(np.fft.rfft(signals_linear[i, icomp, :])), c=f'C{i}', ls='--')  # exact linear

    specX = np.abs(np.fft.rfft(all_signals[i, 0, :]))
    specX_f = specX[np.argmin(np.abs(f - 1 / T0))]
    specX_2f = specX[np.argmin(np.abs(f - 2 / T0))]
    ax[0, 1].annotate('2nd harmonic', xy=(2 / T0, 1.2 * specX_2f),
                      xytext=(2.1 / T0, 0.4 * specX_f),
                      arrowprops=dict(facecolor='black', shrink=0.05))
    ax[-1, 0].set_xlabel('Time')
    ax[-1, 1].set_xlabel('Frequency')
    ax[-1, 1].set_xlim(-0.1 / T0, 3.8 / T0)
    ax[-1, 1].set_xticks(np.arange(4) / T0, ['0', 'f', '2f', '3f'])
    plt.show()
#
# -----------------------------------------------------
../../_images/demos__ln_demo_weqnl_1D-PSV_Murnaghan_Pwave_21_0.png