Axisymmetric Fluid Filled Pipe

This example details a more complex application of the pywfe package to an axisymmetric fluid filled pipe.

The mesh information was initially generated using COMSOL Multiphysics with cyclic symmetry. As such, the system descibes axisymmetric (otherwise known as \(n=0\)) motion. The pipe structure modelled as steel with a hysteretic loss factor of 0.1%, the inner radius is 0.2m and the outer radius is 0.21

Loading and Inspecting the Model

../_images/model_diagram.JPG

The pipe model in this example has been saved into the package database, and can be loaded with

import numpy as np
import pywfe
import matplotlib.pyplot as plt

model = pywfe.load("AXISYM_thick_0pt1pc_damping", source='database')

The model description, which is optionally added before saving returns:

print(model.description)

>>> WFE Segment of water filled pipe
>>>
>>> Inner radius: 20cm, Outer radius: 21cm
>>> Steel Youngs mod: 19.2e10*(1 + eta_s*i), Poissons ratio: 0.3, Steel density: 7850,
>>> Water bulk mod: 2.1e9*(1 + eta_f*i), Water density: 1000
>>>
>>> Steel loss-factor = 0.001
>>> Water loss-factor = 0
>>> maximum element size = 1cm

>>> Outer radial forcing dof index: 45

With pywfe.Model.see() the model mesh can be visualised, showing the left face of the segment. In this case, since the model is 2D and axisymmetric, the left face is a line of nodes.

../_images/model_see.JPG

The dof attribute contains more information about the degrees of freedom. For example, print(set(model.dof['fieldvar'])) will show the unique field variables in the model which are 'u', 'w' 'p'. These are the two structural (radial and longitdunal) degrees of freedom in the pipe wall and the pressure in the pipe fluid respectively. Clicking on the nodes displayed with pywfe.Model.see() using an interactive matplotlib backend will print information about that node.

Free Wave Solutions

Dispersion Curves

As with the (Analytical Beam Example), the dispersion curves of the system can be calculated:

f_arr = np.linspace(10, 10e3, 300)

k = model.dispersion_relation(f_arr)

plt.subplot(2, 1, 1)
plt.plot(f_arr, k.real, '.')
plt.ylabel('Re(k)')
plt.ylim(0, 50)

plt.subplot(2, 1, 2)
plt.plot(f_arr, k.imag, '.')
plt.ylabel('Im(k)')
plt.ylim(0, -50)

plt.xlabel('Frequency (Hz)')
../_images/wavenumber_scatter.JPG

The solutions wavenumber solutions are not sorted, and so are plotted as a scatter plot. The phase velocity curves can also be computed with pywfe.Model.phase_velocity(). In this case, the wavenumbers are already computed, and the phase velocity can be calculated via its definition. Only strongly propagating modes are plotted:

k_prop = np.copy(k)
k_prop[abs(k.imag) > 0.5] = np.nan

c_p = 2*np.pi*f_arr[:, None]/k_prop

plt.plot(f_arr, c_p, '.')
plt.ylim(0, 8e3)

plt.xlabel('Frequency (Hz)')
../_images/cp_scatter.JPG

Mode Shapes

Also of interest for the free-wave solutions are the mode shapes. These could be calculated for each frequency with pywfe.Model.generate_eigensolution(), which gives the raw eigensolution. Instead, it is convenient to use the pywfe.Model.frequency_sweep() method, which allows many different frequency dependent quantities to be solved together with each frequency step. The positive-going wavenumbers and mode shapes are requested for the frequency sweep, and are stored in a dictionary.

sweep_result = model.frequency_sweep(
f_arr, quantities=['wavenumbers', 'phi_plus'])

NOTE: The pywfe.Model.frequency_sweep() method allows the modal assurance criterion to be used to track each mode across sufficiently fine frequency steps.

sweep_result = model.frequency_sweep(
f_arr, quantities=['wavenumbers', 'phi_plus'], mac = True)

Now the dispersion relation can be plotted with continuous curves in the wavenumber domain:

plt.subplot(2, 1, 1)
plt.plot(f_arr, sweep_result['wavenumbers'].real)
plt.ylabel('Re(k)')
plt.ylim(0, 50)
plt.subplot(2, 1, 2)
plt.plot(f_arr, sweep_result['wavenumbers'].imag)
plt.ylabel('Im(k)')

plt.xlabel('Frequency (Hz)')
plt.ylim(0, -50)
../_images/wavenumber_trace.JPG

The mode shapes from the frequency sweep have the shape (n. frequencies, n. dofs, n.modes).

phi = np.copy(sweep_result['phi_plus'])

print(phi.shape)
>>> (300, 94, 47)

The first half of the dof axis represents the free-wave modal displacements, and the second half the forces. We select just the displacement part of the mode shapes with

# get just the displacement component of the mode shapes by slicing down the second axis
phi_q = phi[:, :model.N//2, :]

Selecting Degrees of Freedom

The displacement mode shapes contain both the structural displacements and pressures. To separate these out, the method pywfe.Model.select_dofs() is provided. The dofs are selected by their field variable with:

struc_dof = model.select_dofs(fieldvar=['u', 'w'])
fluid_dof = model.select_dofs(fieldvar='p')

which returns a reduced dof dictionary for each selection. To select the part the corresponding part of the mode shape array, pywfe.Model.dof_to_indices() is used:

fluid_dof_indices = model.dof_to_indices(fluid_dof)
phi_p = phi_q[fluid_dof_indices]

phi_p now represents the pressure mode shapes.

Mode Sorting

Before plotting the mode shapes, There is one more useful function for sorting the free-wave solutions. The function pywfe.sort_wavenumbers() can be used on a wavenumber solution to produce sorted indices for modes according to their order of cut-on.

The free-wave solutions can now be sorted along the modal axis with

sorted_mode_indices = pywfe.sort_wavenumbers(sweep_result['wavenumbers'])

k_sorted = np.copy(sweep_result['wavenumbers'])[..., sorted_mode_indices]
phi_p_sorted = np.copy(phi_p)[..., sorted_mode_indices]

np.copy is used to keep the sorted and unsorted solutions separate to avoid confusion. The pressure mode shapes for the first two propagating modes are now plotted at a low frequency:

radial_coord = fluid_dof['coord'][1]
frequency_index = 10

for mode_index in [0, 1]:

    plt.subplot(2, 1, 1)

    plt.plot(radial_coord, phi_p_sorted[frequency_index, :, mode_index])
    plt.axhline(y=0)
    plt.xlabel('radial coordinate (m)')
    plt.ylabel('pressure (arb)')

    plt.subplot(2, 1, 2)

    plt.plot(f_arr, 2*np.pi*f_arr /
            k_sorted[..., mode_index], label=f'mode {mode_index}')
    plt.axvline(x=f_arr[frequency_index], color='black')
    plt.xlabel('frequency (Hz)')
    plt.ylabel('Phase velocity (m/s)')

plt.legend(loc='best')
plt.suptitle(f'Frequency: {f_arr[frequency_index]:.0f} Hz')
plt.tight_layout()
plt.title()
../_images/first_two_modes.JPG

Now all 5 propagating pressure modes at the maximum frequency of interest are plotted:

frequency_index = -1

# plot only the propagating wavenumbers
k_sorted_propagating = np.copy(k_sorted)
k_sorted_propagating[abs(k_sorted.imag) > 0.5] = np.nan

for mode_index in [0, 1, 2, 3, 4]:

    plt.subplot(2, 1, 1)

    plt.plot(radial_coord, phi_p_sorted[frequency_index, :, mode_index])
    plt.axhline(y=0, color='black', linestyle=':')
    plt.xlabel('radial coordinate (m)')
    plt.ylabel('pressure (arb)')

    plt.subplot(2, 1, 2)

    plt.plot(f_arr, 2*np.pi*f_arr /
            k_sorted_propagating[..., mode_index], label=f'{mode_index + 1}')

    plt.axvline(x=f_arr[frequency_index], color='black')
    plt.xlabel('frequency (Hz)')
    plt.ylabel('Phase velocity (m/s)')

plt.subplot(2, 1, 2)
plt.ylim(0, 10e3)
plt.legend(loc='best', ncols=5)
../_images/all_prop_modes.JPG

Model Forcing

We now add a radial force to the outer pipe wall with the appropriate degree of freedom.

# add a 1 newton radial force to the outer pipe wall
model.force[45] = 1

With this loading added the forced response can be calculated with a number of methods. For example, to calculate the pressure field at x=0:

# plot the pressure across the the radial coordinate at x=0
excitation_frequency = 15e3

p0 = model.displacements(f=excitation_frequency, x_r=0, dofs=fluid_dof)

plt.plot(radial_coord, p0)
plt.xlabel('radial coordinate (m)')
plt.ylabel('pressure (Pa)')
plt.title(f'frequency: {excitation_frequency} Hz')
../_images/pressure_x0.JPG

Or to calculate the radial displacement at the outer wall (the same dof at which the forcing was applied):

excitation_frequency = 1000

x_arr = np.linspace(0, 100, 1000)

u_x = model.displacements(f=excitation_frequency, x_r=x_arr, dofs=[45])

plt.plot(x_arr, u_x)
plt.xlabel('axial coordinate (m)')
plt.ylabel('displacement (m)')
plt.title(f'frequency: {excitation_frequency} Hz')
../_images/displacement_x.JPG

To calculate the input mobility, we use pywfe.Model.tranfer_function() at x=0 over a given frequency range with derivative=1 to return the structural velocity (the input force is 1N)

input_mobility = model.transfer_function(f_arr, x_r=0, dofs=[45], derivative=1)

plt.semilogy(f_arr, abs(input_mobility))
plt.xlabel('Frequency (Hz)')
plt.ylabel('input mobility (m/Ns)')
../_images/input_mobility.JPG

Finally, to allow easier visualisation of results without wrestling with matplotlib, a displacement/pressure field can be saved into the .vtu format for loading into ParaView. See pwfe.save_as_vtk(). Here we save the pressure field from 0-2m at an excitation frequency of 4kHz

../_images/paraview_output.JPG