Implementation#

Author: Antonio Baiano Svizzero and Jørgen S. Dokken

In this tutorial, you will learn how to:

  • Define acoustic velocity and impedance boundary conditions

  • Compute acoustic sound pressure for multiple frequencies

  • Compute the Sound Pressure Level (SPL) at a given microphone position

Test problem#

As an example, we will model a plane wave propagating in a tube. While it is a basic test case, the code can be adapted to way more complex problems where velocity and impedance boundary conditions are needed. We will apply a velocity boundary condition \(v_n = 0.001\) to one end of the tube (for the sake of simplicity, in this basic example, we are ignoring the point source, which can be applied with scifem) and an impedance \(Z\) computed with the Delaney-Bazley model, supposing that a layer of thickness \(d = 0.02\) and flow resistivity \(\sigma = 1e4\) is placed at the second end of the tube. The choice of such impedance (the one of a plane wave propagating in free field) will give, as a result, a solution with no reflections.

First, we create the mesh with gmsh, also setting the physical group for velocity and impedance boundary conditions and the respective tags.

import gmsh

gmsh.initialize()

# meshsize settings
meshsize = 0.02
gmsh.option.setNumber("Mesh.MeshSizeMax", meshsize)
gmsh.option.setNumber("Mesh.MeshSizeMax", meshsize)


# create geometry
L = 1
W = 0.1

gmsh.model.occ.addBox(0, 0, 0, L, W, W)
gmsh.model.occ.synchronize()

# setup physical groups
v_bc_tag = 2
Z_bc_tag = 3
gmsh.model.addPhysicalGroup(3, [1], 1, "air_volume")
gmsh.model.addPhysicalGroup(2, [1], v_bc_tag, "velocity_BC")
gmsh.model.addPhysicalGroup(2, [2], Z_bc_tag, "impedance")

# mesh generation
gmsh.model.mesh.generate(3)
Info    : Meshing 1D...
Info    : [  0%] Meshing curve 1 (Line)
Info    : [ 10%] Meshing curve 2 (Line)
Info    : [ 20%] Meshing curve 3 (Line)
Info    : [ 30%] Meshing curve 4 (Line)
Info    : [ 40%] Meshing curve 5 (Line)
Info    : [ 50%] Meshing curve 6 (Line)
Info    : [ 60%] Meshing curve 7 (Line)
Info    : [ 60%] Meshing curve 8 (Line)
Info    : [ 70%] Meshing curve 9 (Line)
Info    : [ 80%] Meshing curve 10 (Line)
Info    : [ 90%] Meshing curve 11 (Line)
Info    : [100%] Meshing curve 12 (Line)
Info    : Done meshing 1D (Wall 0.00109824s, CPU 0.000536s)
Info    : Meshing 2D...
Info    : [  0%] Meshing surface 1 (Plane, Frontal-Delaunay)
Info    : [ 20%] Meshing surface 2 (Plane, Frontal-Delaunay)
Info    : [ 40%] Meshing surface 3 (Plane, Frontal-Delaunay)
Info    : [ 60%] Meshing surface 4 (Plane, Frontal-Delaunay)
Info    : [ 70%] Meshing surface 5 (Plane, Frontal-Delaunay)
Info    : [ 90%] Meshing surface 6 (Plane, Frontal-Delaunay)
Info    : Done meshing 2D (Wall 0.0320734s, CPU 0.032226s)
Info    : Meshing 3D...
Info    : 3D Meshing 1 volume with 1 connected component
Info    : Tetrahedrizing 1285 nodes...
Info    : Done tetrahedrizing 1293 nodes (Wall 0.0136336s, CPU 0.012747s)
Info    : Reconstructing mesh...
Info    :  - Creating surface mesh
Info    :  - Identifying boundary edges
Info    :  - Recovering boundary
Info    : Done reconstructing mesh (Wall 0.0282127s, CPU 0.025398s)
Info    : Found volume 1
Info    : It. 0 - 0 nodes created - worst tet radius 2.85527 (nodes removed 0 0)
Info    : 3D refinement terminated (1746 nodes total):
Info    :  - 0 Delaunay cavities modified for star shapeness
Info    :  - 0 nodes could not be inserted
Info    :  - 6588 tetrahedra created in 0.0190342 sec. (346114 tets/s)
Info    : 0 node relocations
Info    : Done meshing 3D (Wall 0.0767936s, CPU 0.074166s)
Info    : Optimizing mesh...
Info    : Optimizing volume 1
Info    : Optimization starts (volume = 0.01) with worst = 0.0138257 / average = 0.769909:
Info    : 0.00 < quality < 0.10 :        18 elements
Info    : 0.10 < quality < 0.20 :        69 elements
Info    : 0.20 < quality < 0.30 :        66 elements
Info    : 0.30 < quality < 0.40 :        67 elements
Info    : 0.40 < quality < 0.50 :       150 elements
Info    : 0.50 < quality < 0.60 :       273 elements
Info    : 0.60 < quality < 0.70 :       913 elements
Info    : 0.70 < quality < 0.80 :      1842 elements
Info    : 0.80 < quality < 0.90 :      2090 elements
Info    : 0.90 < quality < 1.00 :      1095 elements
Info    : 152 edge swaps, 0 node relocations (volume = 0.01): worst = 0.300511 / average = 0.784453 (Wall 0.00189394s, CPU 0.001951s)
Info    : No ill-shaped tets in the mesh :-)
Info    : 0.00 < quality < 0.10 :         0 elements
Info    : 0.10 < quality < 0.20 :         0 elements
Info    : 0.20 < quality < 0.30 :         0 elements
Info    : 0.30 < quality < 0.40 :        67 elements
Info    : 0.40 < quality < 0.50 :       135 elements
Info    : 0.50 < quality < 0.60 :       271 elements
Info    : 0.60 < quality < 0.70 :       907 elements
Info    : 0.70 < quality < 0.80 :      1872 elements
Info    : 0.80 < quality < 0.90 :      2113 elements
Info    : 0.90 < quality < 1.00 :      1081 elements
Info    : Done optimizing mesh (Wall 0.00793662s, CPU 0.0071s)
Info    : 1746 nodes 9265 elements

Then we import the gmsh mesh with the dolfinx.io.gmshio function.

from mpi4py import MPI
from dolfinx import (
    fem,
    io,
    default_scalar_type,
    geometry,
    __version__ as dolfinx_version,
)
from dolfinx.fem.petsc import LinearProblem
import ufl
import numpy as np
import numpy.typing as npt
from packaging.version import Version

mesh_data = io.gmshio.model_to_mesh(gmsh.model, MPI.COMM_WORLD, 0, gdim=3)
if Version(dolfinx_version) > Version("0.9.0"):
    domain = mesh_data.mesh
    assert mesh_data.facet_tags is not None
    facet_tags = mesh_data.facet_tags
else:
    domain, _, facet_tags = mesh_data

We define the function space for our unknown \(p\) and define the range of frequencies we want to solve the Helmholtz equation for.

V = fem.functionspace(domain, ("Lagrange", 1))

# Discrete frequency range
freq = np.arange(10, 1000, 5)  # Hz

# Air parameters
rho0 = 1.225  # kg/m^3
c = 340  # m/s

Boundary conditions#

The Delaney-Bazley model is used to compute the characteristic impedance and wavenumber of the porous layer, treated as an equivalent fluid with complex valued properties

\[\begin{split} \begin{align} Z_c(\omega) &= \rho_0 c_0 \left[1 + 0.0571 X^{-0.754} - j 0.087 X^{-0.732}\right],\\ k_c(\omega) &= \frac{\omega}{c_0} \left[1 + 0.0978 X^{-0.700} - j 0.189 X^{-0.595}\right],\\ \end{align} \end{split}\]

where \(X = \frac{\rho_0 f}{\sigma}\).

With these, we can compute the surface impedance, that in the case of a rigid passive absorber placed on a rigid wall is given by the formula $$

(43)#\[\begin{align} Z_s = -j Z_c cot(k_c d). \end{align}\]

$$

Let’s create a function to compute it.

# Impedance calculation
def delany_bazley_layer(f, rho0, c, sigma):
    X = rho0 * f / sigma
    Zc = rho0 * c * (1 + 0.0571 * X**-0.754 - 1j * 0.087 * X**-0.732)
    kc = 2 * np.pi * f / c * (1 + 0.0978 * (X**-0.700) - 1j * 0.189 * (X**-0.595))
    Z_s = -1j * Zc * (1 / np.tan(kc * d))
    return Z_s


sigma = 1.5e4
d = 0.01
Z_s = delany_bazley_layer(freq, rho0, c, sigma)

Since we are going to compute a sound pressure spectrum, all the variables that depend on frequency (that are \(\omega\), \(k\) and \(Z\)) need to be updated in the frequency loop. To make this possible, we will initialize them as dolfinx constants. Then, we define the value for the normal velocity on the first end of the tube

omega = fem.Constant(domain, default_scalar_type(0))
k = fem.Constant(domain, default_scalar_type(0))
Z = fem.Constant(domain, default_scalar_type(0))
v_n = 1e-5

We also need to specify the integration measure \(ds\), by using ufl, and its built in integration measures

ds = ufl.Measure("ds", domain=domain, subdomain_data=facet_tags)

Variational Formulation#

We can now write the variational formulation.

p = ufl.TrialFunction(V)
v = ufl.TestFunction(V)

a = (
    ufl.inner(ufl.grad(p), ufl.grad(v)) * ufl.dx
    + 1j * rho0 * omega / Z * ufl.inner(p, v) * ds(Z_bc_tag)
    - k**2 * ufl.inner(p, v) * ufl.dx
)
L = -1j * omega * rho0 * ufl.inner(v_n, v) * ds(v_bc_tag)

The class LinearProblem is used to setup the PETSc backend and assemble the system vector and matrices. The solution will be stored in a dolfinx.fem.Function, p_a.

p_a = fem.Function(V)
p_a.name = "pressure"

problem = LinearProblem(
    a,
    L,
    u=p_a,
    petsc_options={
        "ksp_type": "preonly",
        "pc_type": "lu",
        "pc_factor_mat_solver_type": "mumps",
    },
)

Computing the pressure at a given location#

Before starting our frequency loop, we can build a function that, given a microphone position, computes the sound pressure at its location. We will use the a similar method as in Deflection of a membrane. However, as the domain doesn’t deform in time, we cache the collision detection

class MicrophonePressure:
    def __init__(self, domain, microphone_position):
        """Initialize microphone(s).

        Args:
            domain: The domain to insert microphones on
            microphone_position: Position of the microphone(s).
                Assumed to be ordered as ``(mic0_x, mic1_x, ..., mic0_y, mic1_y, ..., mic0_z, mic1_z, ...)``

        """
        self._domain = domain
        self._position = np.asarray(
            microphone_position, dtype=domain.geometry.x.dtype
        ).reshape(3, -1)
        self._local_cells, self._local_position = self.compute_local_microphones()

    def compute_local_microphones(
        self,
    ) -> tuple[npt.NDArray[np.int32], npt.NDArray[np.floating]]:
        """
        Compute the local microphone positions for a distributed mesh

        Returns:
            Two lists (local_cells, local_points) containing the local cell indices and the local points
        """
        points = self._position.T
        bb_tree = geometry.bb_tree(self._domain, self._domain.topology.dim)

        cells = []
        points_on_proc = []

        cell_candidates = geometry.compute_collisions_points(bb_tree, points)
        colliding_cells = geometry.compute_colliding_cells(
            domain, cell_candidates, points
        )

        for i, point in enumerate(points):
            if len(colliding_cells.links(i)) > 0:
                points_on_proc.append(point)
                cells.append(colliding_cells.links(i)[0])

        return np.asarray(cells, dtype=np.int32), np.asarray(
            points_on_proc, dtype=domain.geometry.x.dtype
        )

    def listen(
        self, recompute_collisions: bool = False
    ) -> npt.NDArray[np.complexfloating]:
        if recompute_collisions:
            self._local_cells, self._local_position = self.compute_local_microphones()
        if len(self._local_cells) > 0:
            return p_a.eval(self._local_position, self._local_cells)
        else:
            return np.zeros(0, dtype=default_scalar_type)

The pressure spectrum is initialized as a numpy array and the microphone location is assigned

p_mic = np.zeros((len(freq), 1), dtype=complex)

mic = np.array([0.5, 0.05, 0.05])
microphone = MicrophonePressure(domain, mic)

Frequency loop#

Finally, we can write the frequency loop, where we update the values of the frequency-dependent variables and solve the system for each frequency

for nf in range(0, len(freq)):
    k.value = 2 * np.pi * freq[nf] / c
    omega.value = 2 * np.pi * freq[nf]
    Z.value = Z_s[nf]

    problem.solve()
    p_a.x.scatter_forward()

    p_f = microphone.listen()
    p_f = domain.comm.gather(p_f, root=0)

    if domain.comm.rank == 0:
        assert p_f is not None
        p_mic[nf] = np.hstack(p_f)

SPL spectrum#

After the computation, the pressure spectrum at the prescribed location is available. Such a spectrum is usually shown using the decibel (dB) scale to obtain the SPL, with the RMS pressure as input, defined as \(p_{rms} = \frac{p}{\sqrt{2}}\).

if domain.comm.rank == 0:
    import matplotlib.pyplot as plt

    fig = plt.figure(figsize=(25, 8))
    plt.plot(freq, 20 * np.log10(np.abs(p_mic) / np.sqrt(2) / 2e-5), linewidth=2)
    plt.grid(True)
    plt.xlabel("Frequency [Hz]")
    plt.ylabel("SPL [dB]")
    plt.xlim([freq[0], freq[-1]])
    plt.ylim([0, 90])
    plt.legend()
    plt.show()
../_images/ef44ad18dba26d343bfa9ac41d93401bc15788b4eb73e802331733edb5de887f.png