#!/usr/bin/env python
# coding: utf-8
# This tutorial forms part of the documentation for Bempp. Find out more at bempp.com
# # Scattering from a sphere using a combined direct formulation
# ### Background
# In this tutorial, we will solve the problem of scattering from the unit sphere $\Omega$ using a combined integral formulation and an incident wave defined by
#
# $$
# u^{\text{inc}}(\mathbf x) = \mathrm{e}^{\mathrm{i} k x}.
# $$
#
# where $\mathbf x = (x, y, z)$.
#
# The PDE is given by the Helmholtz equation:
#
# $$
# \Delta u + k^2 u = 0, \quad \text{ in } \mathbb{R}^3 \backslash \Omega,
# $$
#
# where $u=u^\text{s}+u^\text{inc}$ is the total acoustic field and $u^\text{s}$ satisfies the Sommerfeld radiation condition
#
# $$
# \frac{\partial u^\text{s}}{\partial r}-\mathrm{i}ku^\text{s}=o(r^{-1})
# $$
#
# for $r:=|\mathbf{x}|\rightarrow\infty$.
#
# From Green's representation formula, one can derive that
#
# $$
# u(\mathbf x) = u^\text{inc}-\int_{\Gamma}g(\mathbf x,\mathbf y)\frac{\partial u}{\partial\nu}(\mathbf y)\mathrm{d}\mathbf{y}.
# $$
#
# Here, $g(\mathbf x, \mathbf y)$ is the acoustic Green's function given by
#
# $$
# g(\mathbf x, \mathbf y):=\frac{\mathrm{e}^{\mathrm{i} k |\mathbf{x}-\mathbf{y}|}}{4 \pi |\mathbf{x}-\mathbf{y}|}.
# $$
#
# The problem has therefore been reduced to computing the normal derivative $u_\nu:=\frac{\partial u}{\partial\nu}$ on the boundary $\Gamma$. This is achieved using the following boundary integral equation formulation.
#
# $$
# (\tfrac12\mathsf{Id} + \mathsf{K}' - \mathrm{i} \eta \mathsf{V}) u_\nu(\mathbf{x}) = \frac{\partial u^{\text{inc}}}{\partial \nu}(\mathbf{x}) - \mathrm{i} \eta u^{\text{inc}}(\mathbf{x}), \quad \mathbf{x} \in \Gamma.
# $$
#
# where $\mathsf{Id}$, $\mathsf{K}'$ and $\mathsf{V}$ are identity, adjoint double layer and single layer boundary operators. More details of the derivation of this formulation and its properties can be found in the article Chandler-Wilde et al (2012).
#
# ### Implementation
# First we import the Bempp module and NumPy.
# In[1]:
import bempp.api
import numpy as np
# We define the wavenumber
# In[2]:
k = 15.
# The following command creates a sphere mesh.
# In[4]:
grid = bempp.api.shapes.regular_sphere(5)
# As basis functions, we use piecewise constant functions over the elements of the mesh. The corresponding space is initialised as follows.
# In[6]:
piecewise_const_space = bempp.api.function_space(grid, "DP", 0)
# We now initialise the boundary operators.
# A boundary operator always takes at least three space arguments: a domain space, a range space and the test space (dual to the range). In this example we only work on the space $\mathcal{L}^2(\Gamma)$ and we can choose all spaces to be identical.
# In[7]:
identity = bempp.api.operators.boundary.sparse.identity(
piecewise_const_space, piecewise_const_space, piecewise_const_space)
adlp = bempp.api.operators.boundary.helmholtz.adjoint_double_layer(
piecewise_const_space, piecewise_const_space, piecewise_const_space, k)
slp = bempp.api.operators.boundary.helmholtz.single_layer(
piecewise_const_space, piecewise_const_space, piecewise_const_space, k)
# Standard arithmetic operators can be used to create linear combinations of boundary operators.
# In[8]:
lhs = 0.5 * identity + adlp - 1j * k * slp
# We now form the right-hand side by defining a GridFunction using Python callable.
# In[9]:
def combined_data(x, n, domain_index, result):
result[0] = 1j * k * np.exp(1j * k * x[0]) * (n[0]-1)
grid_fun = bempp.api.GridFunction(piecewise_const_space, fun=combined_data)
# We can now use GMRES to solve the problem.
# In[10]:
from bempp.api.linalg import gmres
neumann_fun, info = gmres(lhs, grid_fun, tol=1E-5)
# `gmres` returns a grid function `neumann_fun` and an integer `info`. When everything works fine info is equal to 0.
#
# At this stage, we have the surface solution of the integral equation. Now we will evaluate the solution in the domain of interest. We define the evaluation points as follows.
# In[12]:
Nx = 200
Ny = 200
xmin, xmax, ymin, ymax = [-3, 3, -3, 3]
plot_grid = np.mgrid[xmin:xmax:Nx * 1j, ymin:ymax:Ny * 1j]
points = np.vstack((plot_grid[0].ravel(),
plot_grid[1].ravel(),
np.zeros(plot_grid[0].size)))
u_evaluated = np.zeros(points.shape[1], dtype=np.complex128)
u_evaluated[:] = np.nan
# This will generate a grid of points in the $x$-$y$ plane.
#
# Then we create a single layer potential operator and use it to evaluate the solution at the evaluation points. The variable ``idx`` allows to compute the solution only at points located outside the unit circle of the plane. We use a single layer potential operator to evaluate the solution at the observation points.
# In[13]:
x, y, z = points
idx = np.sqrt(x**2 + y**2) > 1.0
from bempp.api.operators.potential import helmholtz as helmholtz_potential
slp_pot = helmholtz_potential.single_layer(
piecewise_const_space, points[:, idx], k)
res = np.real(np.exp(1j *k * points[0, idx]) - slp_pot.evaluate(neumann_fun))
u_evaluated[idx] = res.flat
# We now plot the slice of the domain solution.
# In[17]:
get_ipython().run_line_magic('matplotlib', 'inline')
u_evaluated = u_evaluated.reshape((Nx, Ny))
from matplotlib import pyplot as plt
fig = plt.figure(figsize=(10, 8))
plt.imshow(np.real(u_evaluated.T), extent=[-3, 3, -3, 3])
plt.xlabel('x')
plt.ylabel('y')
plt.colorbar()
plt.title("Scattering from the unit sphere, solution in plane z=0")