FEniCS, pygmsh and boundary conditions

Late this night … I can’t believe it, I found the error. One has to use:

sphere_inner = geom.add_ball([sphere_x1, sphere_y1, sphere_z1], sphere_r1)
sphere_outer = geom.add_ball([sphere_x2, sphere_y2, sphere_z2], sphere_r2)
sphere_final = geom.boolean_difference([sphere_outer],[sphere_inner], delete_first=True, delete_other=True)
geom.add_raw_code("Physical Surface(\"1\") = {1};") # inner
geom.add_raw_code("Physical Surface(\"2\") = {2};") # outer
geom.add_raw_code("Physical Volume(\"3\") = {" + sphere_final.id + "};") # volume

The numbers on the right side of Physical Surface(\"...\") were wrong, they are now correct (= {1} and = {2}).

Here is a complete code, which shall demonstrate, how to calculate the electrostatic potential/field of a spherical capacitor … . Have fun with the example and thanks again for the help!

# ##### BEGIN GPL LICENSE BLOCK #####
#
#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU General Public License
#  as published by the Free Software Foundation; either version 2
#  of the License, or (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software Foundation,
#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
#
#  The electrostatic potential and field of a spherical capacitor.
#
#
#  Clemens Barth
#
#
#  Start             : 2020-04-01
#  Last modified     : 2020-04-01
#
#

import pygmsh
import meshio
from fenics import *

import matplotlib.pyplot as plt
import numpy as np
import scipy.constants as const

# _____________________________________________________ The values to play with

# The potentials on the boundaries (inner and outer sphere)
pot_1 = 1.0 # inner
pot_2 = 0.0 # outer

# The position and size of the two spheres
sphere_x1 =   0.0
sphere_y1 =   0.0
sphere_z1 =   0.0
sphere_r1 =  10.0

sphere_x2 =   0.0
sphere_y2 =   0.0
sphere_z2 =   0.0
sphere_r2 = 100.0

# ____________________________________________________________________ The code

# The file name
filename_base = "Sphere-sphere-capacity_v06"

# Build the mesh first with pygmsh
geom = pygmsh.opencascade.Geometry(characteristic_length_min=10.0,
                                   characteristic_length_max=10.0)

# Create the spheres and ...
sphere_inner = geom.add_ball([sphere_x1, sphere_y1, sphere_z1], sphere_r1)
sphere_outer = geom.add_ball([sphere_x2, sphere_y2, sphere_z2], sphere_r2)
# ... take the difference (boolean).
sphere_final = geom.boolean_difference([sphere_outer],[sphere_inner],
                                       delete_first=True,
                                       delete_other=True)

# The numbers 2, 3, ... 7 are identifiers of subsurfaces. They can be seen in
# Gmsh as follows (open the .geo file):
#    1. Key 'Alt-s' for surface, 'Alt-v' for volume
#    2. Hover over the dashed lines and read the values in the small rectangle.
geom.add_raw_code("Physical Surface(\"1\") = {1};") # inner sphere
geom.add_raw_code("Physical Surface(\"2\") = {2};") # outer sphere
geom.add_raw_code("Physical Volume(\"3\") = {" + sphere_final.id + "};") # vol.

# Build the .geo file.
mesh = pygmsh.generate_mesh(geom, geo_filename=filename_base+".geo")

# What follows is this (strange) mesh conversion stuff ... . ;-)
tetra_cells    = None
tetra_data     = None
triangle_cells = None
triangle_data  = None

for key in mesh.cell_data_dict["gmsh:physical"].keys():
    if key == "triangle":
        triangle_data = mesh.cell_data_dict["gmsh:physical"][key]
    elif key == "tetra":
        tetra_data = mesh.cell_data_dict["gmsh:physical"][key]
for cell in mesh.cells:
    if cell.type == "tetra":
        if tetra_cells is None:
            tetra_cells = cell.data
        else:
            tetra_cells = np.vstack([tetra_cells, cell.data])
    elif cell.type == "triangle":
        if triangle_cells is None:
            triangle_cells = cell.data
        else:
            triangle_cells = np.vstack([triangle_cells, cell.data])

tetra_mesh = meshio.Mesh(points=mesh.points,
                         cells={"tetra": tetra_cells},
                         cell_data={"name_to_read":[tetra_data]})
triangle_mesh =meshio.Mesh(points=mesh.points,
                           cells=[("triangle", triangle_cells)],
                           cell_data={"name_to_read":[triangle_data]})
meshio.write(filename_base+"_tetra_mesh.xdmf", tetra_mesh)
meshio.write(filename_base+"_mf.xdmf", triangle_mesh)

# All the latter .xdmf files are read such that they can be used in FEniCS.
mesh = Mesh()
with XDMFFile(filename_base+"_tetra_mesh.xdmf") as infile:
    infile.read(mesh)

mvc = MeshValueCollection("size_t", mesh, 2)
with XDMFFile(filename_base+"_mf.xdmf") as infile:
    infile.read(mvc, "name_to_read")

mf = cpp.mesh.MeshFunctionSizet(mesh, mvc)

# The mesh function.
V = FunctionSpace(mesh, 'CG', 2)

# Set the boundary conditions using the boundary numbers specified in Gmsh.
inner_boundary = DirichletBC(V, Constant(pot_1), mf, 1)
outer_boundary = DirichletBC(V, Constant(pot_2), mf, 2)
bcs =[inner_boundary, outer_boundary]

# Solve the Poisson equation with the source set to 0
# (the Laplace equation) via the L = Constant('0')... line
u = TrialFunction(V)
v = TestFunction(V)
a = dot(grad(u), grad(v)) * dx
L = Constant('0') * v * dx
u = Function(V)
solve(a == L, u, bcs)

# Find the electric field by taking the negative of the
# gradient of the electric potential. Project this onto
# the function space for evaluation.
electric_field = project(-grad(u))

# output the results to two separate files
potentialFile = File(filename_base+"_potential.pvd")
potentialFile << u
e_fieldfile   = File(filename_base+"_Efield.pvd")
e_fieldfile   << electric_field

# Obtain a profile with matplotlib.
x      = np.linspace(sphere_r1, sphere_r2-1.0, 1000)
y      = np.zeros(len(x))
z      = np.zeros(len(x))
coords = np.dstack((x, y, z))[0]
# This is the profile of the potential at x=x1..x2, 0, 0
u_line = np.array(list(map(u, coords)))

# We now calculate the analytical solution:
def Analytic_potential(phi_i, phi_o, r_i, r_o, r):
    phi_1 = (phi_i * r_i - phi_o * r_o)   /      (r_i - r_o)
    phi_2 = (r_i * r_o * (phi_i - phi_o)) / (r * (r_i - r_o))
    phi = phi_1 - phi_2
    return phi

# The line along the y axis.
y_analytic = np.linspace(sphere_r1, sphere_r2-1.0, 1000)

# The profile data.
u_line_analytic = [Analytic_potential(pot_1,
                                      pot_2,
                                      sphere_r1,
                                      sphere_r2,
                                      r) for r in y_analytic]

# Plot the potentials and errors
fig, (ax1, ax2) = plt.subplots(2, sharex=True)
fig.suptitle('Results: electrostatic potential of a spherical capacitor')
ax1.plot(y_analytic, u_line         , linewidth=2)
ax1.plot(y_analytic, u_line_analytic, linewidth=2)
ax1.legend(['Pot_numeric', 'Pot_analytic'], loc='upper right')
ax2.plot(y_analytic, (u_line-u_line_analytic), linewidth=2)
ax2.legend(['Pot_numeric - Pot_analytic'], loc='upper right')
plt.savefig(filename_base+"_potential_profiles.png")

Sphere-sphere-capacity_v06_potential_profiles