FEniCS, pygmsh and boundary conditions

Hello.

I’m new to FEniCS and would like to use its fantastic capabilities. I use FEniCS under Lubuntu 18.04 in a docker environment and have therefore FEniCS version 2019.1.0. In the container, I have also installed python3-pip and gmsh, and with pip I installed pygmsh meshio h5py lxml.

I followed the example described here (Example), which deals with the electrostatic potential of a sphere-sphere capacitor. The example works very well! So far so good. :grinning:

Now, I want to create my personal 3D capacitor (a 3D sphere in front of a plane) with the help of pygmsh and got stuck in the description of the boundary conditions. Here is how I start the code:

import pygmsh
import meshio
import numpy as np
from fenics import *

filename_base = "Create_sphere-plane_v02"

geom = pygmsh.opencascade.Geometry(characteristic_length_min=0.3,
                                   characteristic_length_max=0.3)

sphere_x = 0.0
sphere_y = 0.0
sphere_z = 0.0
sphere_r = 1.0

plane_size      =  30.0
plane_thickness =   0.5
plane_z         =  -4.0

plane_x1   =  -plane_size/2.0
plane_y1   =  -plane_size/2.0
plane_z1   =   plane_z
plane_x2   =   plane_size
plane_y2   =   plane_size
plane_z2   =   plane_thickness

sphere    = geom.add_ball([sphere_x, sphere_y, sphere_z], sphere_r)
rectangle = geom.add_box([plane_x1, plane_y1, plane_z1],
                         [plane_x2, plane_y2, plane_z2])

Now, I want to label the two surfaces, the entire one of the sphere (1) and of the plane (2). I have read that one needs to use geom.add_raw_code, but how?

geom.add_raw_code(‘What comes inside here?’)
geom.add_raw_code(‘What comes inside here?’)

After this code, I have read that one has to well-prepare the mesh, which I got from here:

mesh = pygmsh.generate_mesh(geom)
tetra_cells = None
for cell in mesh.cells:
    if cell.type == "tetra":
        # Collect the individual meshes
        if tetra_cells is None:
            tetra_cells = cell.data
        else:
            tetra_cells = np.vstack((tetra_cells, cell.data))

tetra_data = None
for key in mesh.cell_data_dict["gmsh:physical"].keys():
    if key == "tetra":
        tetra_data = mesh.cell_data_dict["gmsh:physical"][key]
# Create tetra mesh with cell data attached
tetra_mesh = meshio.Mesh(points=mesh.points, cells={"tetra": tetra_cells},
                           cell_data={"name_to_read":[tetra_data]})
meshio.write(filename_base+".xdmf", tetra_mesh)

This code works well (without the geom.add_raw_code thing from above), like also the following code needed to read the geometry for FEniCS:

mesh_file = XDMFFile(MPI.comm_world, filename_base+".xdmf")
mesh = Mesh()
mesh_file.read(mesh);

mvc = MeshValueCollection("size_t", mesh, 3)
mesh_file.read(mvc, "name_to_read")
mf = cpp.mesh.MeshFunctionSizet(mesh, mvc)
File(filename_base+".pvd").write(mesh)

So, the questions are:

  1. What must I insert into geom.add_raw_code('What comes inside here?') such that I can assign the entire surface of the sphere to label ‘1’ and the surface of the plane to label ‘2’?

  2. How can I assign these two identifiers, 1 and 2, to two Dirichlet boundaries conditions where the sphere is on potential 1 and the plane on potential 0. So, I would like to something like:

boundaries = … ?
inner_boundary = fn.DirichletBC(V, fn.Constant(1.0), boundaries, 1)
outer_boundary = fn.DirichletBC(V, fn.Constant(0.0), boundaries, 2)

Thanks for some ideas!

As you are using OpenCascade, and not the built in geometry in gmsh, the functionality of pygmsh is slightly reduced.
What you need to add in add_raw_code is the raw gmsh code describing the tags. In your case:

geom.add_raw_code("Physical Surface(1) = { 1 };")
geom.add_raw_code("Physical Surface(2) = {2,3,4,5,6,7};")

I found these surfaces by visual inspection in gmsh by writing the original geometry to file by:

pygmsh.generate_mesh(geom, geo_filename="mesh.geo")

To answer your second question, this is covered in:

Dear dokken,

thanks a lot for your reply. In particular the idea to save the mesh as a .geo file has clarified how I can identify surfaces and that sort by the Gmsh GUI program.

However, now, if I use …

geom.add_raw_code("Physical Surface(1) = { 1 };")
geom.add_raw_code("Physical Surface(2) = {2,3,4,5,6,7};")

… I cannot save the tetra mesh .xdmf file. It comes out that as soon as I use the two geom.add_raw_codes there is no tetra_mesh anymore. Try out yourself and execute the following code:

import pygmsh
import meshio
import numpy as np
from fenics import *

filename_base = "Create_sphere-plane_v03"
geom = pygmsh.opencascade.Geometry(characteristic_length_min=0.5,
                                   characteristic_length_max=0.5)
sphere_x = 0.0
sphere_y = 0.0
sphere_z = 0.0
sphere_r = 1.0
plane_size      =  30.0
plane_thickness =   0.5
plane_z         =  -4.0
plane_x1   =  -plane_size/2.0
plane_y1   =  -plane_size/2.0
plane_z1   =   plane_z
plane_x2   =   plane_size
plane_y2   =   plane_size
plane_z2   =   plane_thickness

sphere    = geom.add_ball([sphere_x, sphere_y, sphere_z], sphere_r)
rectangle = geom.add_box([plane_x1, plane_y1, plane_z1],
                         [plane_x2, plane_y2, plane_z2])

geom.add_raw_code("Physical Surface(1) = { 1 };")
geom.add_raw_code("Physical Surface(2) = {2,3,4,5,6,7};")

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

tetra_cells = None
tetra_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":
        print("______________ found: tetra_data")
        tetra_data = mesh.cell_data_dict["gmsh:physical"][key]
for cell in mesh.cells:
    if cell.type == "tetra":
        tetra_cells = cell.data
        print("______________ found: tetra_cells")
    elif cell.type == "triangle":
        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("plate.xdmf", tetra_mesh)
meshio.write("mf.xdmf", triangle_mesh)

No tetra_data and tetra_cells are found and I obtain the error message:

File "Create_sphere-plane_v03.py", line 57, in <module>
    meshio.write("plate.xdmf", tetra_mesh)
  File "/usr/local/lib/python3.6/dist-packages/meshio/_helpers.py", line 136, in write
    if value.shape[1] != num_nodes_per_cell[key]:
AttributeError: 'NoneType' object has no attribute 'shape'

Is there may be a problem with the string comparison of the values “physical” and “Physical”? Thanks again for some help. :innocent:

No, the point here is that I have not added any physical tags for the cells. You can do that in a similar fashion using geom.add_raw_code("Physical Volume(1) = {...};").
If you do not require cell marking, you can just remove the cell_data option from your tetra mesh and you are good to go.

Thanks again for your reply.

So, you mean, I could use: tetra_mesh = meshio.Mesh(points=mesh.points, cells={"tetra": tetra_cells}), right? Note that one needs option cells=.

However, still, there are no tetra cells and I get the same error message!

Ah, this is a funny thing we gmsh, requiring physical markers for volumes if they are specified for surfaces. These can be added in a simpler fashion.
Additionally, since you are using two separate topologies, they have to be added together with vstack.
I’ve added a complete working example of your code below.

import pygmsh
import meshio
import numpy as np

filename_base = "Create_sphere-plane_v02"

geom = pygmsh.opencascade.Geometry(characteristic_length_min=0.3,
                                   characteristic_length_max=0.3)

sphere_x = 0.0
sphere_y = 0.0
sphere_z = 0.0
sphere_r = 1.0

plane_size      =  30.0
plane_thickness =   0.5
plane_z         =  -4.0

plane_x1   =  -plane_size/2.0
plane_y1   =  -plane_size/2.0
plane_z1   =   plane_z
plane_x2   =   plane_size
plane_y2   =   plane_size
plane_z2   =   plane_thickness

sphere    = geom.add_ball([sphere_x, sphere_y, sphere_z], sphere_r)
rectangle = geom.add_box([plane_x1, plane_y1, plane_z1],
                         [plane_x2, plane_y2, plane_z2])
geom.add_raw_code("Physical Surface(1) = { 1 };")
geom.add_raw_code("Physical Surface(2) = {2,3,4,5,6,7};")
geom.add_raw_code("Physical Volume(1) = {" + sphere.id + "};")
geom.add_raw_code("Physical Volume(2) = {" + rectangle.id +  "};")

mesh = pygmsh.generate_mesh(geom, geo_filename="mesh.geo")
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":
        print("______________ found: tetra_data")
        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("plate.xdmf", tetra_mesh)
meshio.write("mf.xdmf", triangle_mesh)

1 Like

Dear dokken.

Thanks a lot for your kind help! It works also at my place. So far so good!

Before I attack this particular sphere-plane capacitor, I thought to first reproduce the one described here (Spherical capacitor, sphere inside sphere), but this time with meshes created by pygmsh.

Well, I can successfully build the capacity (by boolean subtraction of two spheres). Furthermore, with the help of gmsh, I could found the boundary markers. The code is here:

import pygmsh
import meshio
from fenics import *
set_log_level(LogLevel.ERROR)

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

filename_base = "Sphere-sphere-capacity_v01"

geom = pygmsh.opencascade.Geometry(characteristic_length_min=10.0,
                                   characteristic_length_max=10.0)

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

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])

geom.add_raw_code("Physical Surface(1) = {1,2,3,2};") # inner
geom.add_raw_code("Physical Surface(2) = {4,5,6,5};") # outer
geom.add_raw_code("Physical Volume(1) = {" + sphere_final.id + "};")

mesh = pygmsh.generate_mesh(geom, geo_filename=filename_base+".geo")

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)

So, now it comes to read the data for FEniCS, which I do as follows (I found the code somewhere on this site):

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)

mvc2 = MeshValueCollection("size_t", mesh, 3)
with XDMFFile(filename_base+"_tetra_mesh.xdmf") as infile:
    infile.read(mvc2, "name_to_read")
cf = cpp.mesh.MeshFunctionSizet(mesh, mvc2) 

The objective now is to put the outer spherical surface potential onto 0 and the inner potential onto 1 (so, these are 2 Dirichlet conditions).

dx = Measure("ds", domain=mesh, subdomain_data=mvc)
V = FunctionSpace(mesh, 'CG', 1)
inner_boundary = DirichletBC(V, Constant(1.0), mf, 1)
outer_boundary = DirichletBC(V, Constant(0.0), mf, 2)
bcs =[inner_boundary, outer_boundary]

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)

As you can imagine, it does not work. :smiling_face_with_three_hearts:

I have somewhat not yet understood where the boundary conditions are included in mvc, mf, cf, and how to apply them onto DirichletBC. What do mvc, mf, cf and these things actually mean? Is there a precise description in the internet since the code is different to the ‘standard’ geo_physical_region.xml and geo_facet_region.xml thing?

Thanks a lot again for some help.

There are several things that stand out to me.
You define a surface measure as your volume measure when it should be:
dx = Measure("dx", domain=mesh, subdomain_data=cf) and
ds=Measure("ds", domain=mesh, subdomain_data=mf).
In your case, since you use dirichlet conditions on all boundaries, ds is not required.
As far as I can see, the Dirichlet conditions are correct.

2 Likes

Dear dokken,

I have to thank you again a lot for your kind help! You are absolutely right, the surface measure ds is not needed. BTW, the cf data is always for the volume and the mf one for the surface?

Anyway, meanwhile I have found out that something with the mesh is not right, here the code:

import pygmsh
import meshio
import numpy as np
import scipy.constants as const
import os

filename_base = "Sphere-sphere-capacity_v05"

geom = pygmsh.opencascade.Geometry(characteristic_length_min=10.0,
                                   characteristic_length_max=10.0)
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

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)

# The numbers 2, 3, ... 7 are identifiers of subsurfaces. They can be seen in
# Gmsh under:
# 1a. Mouse button1 double-klick -> menu -> Geometry visibility -> Surfaces and Volumes.
# 1b. or: Alt-s, Alt-v
# 2. Hover over the dashed lines and read the values in the small rectangle.
geom.add_raw_code("Physical Surface(1) = {1,2,3,2};") # inner
geom.add_raw_code("Physical Surface(2) = {4,5,6,5};") # outer
geom.add_raw_code("Physical Volume(4) = {" + sphere_final.id + "};") # volume

mesh = pygmsh.generate_mesh(geom, geo_filename=filename_base+".geo")
string_os = "gmsh " + filename_base + ".geo -format msh2 -3"
os.system(string_os)
string_os = "dolfin-convert " + filename_base + ".msh " + filename_base + ".xml"
os.system(string_os)

As you can see, I now make use of the old way via gmsh and dolfin-convert in the terminal, to create the mesh .xml file as well as the ...facet_region.xml and physical_region.xml files. It is just a test.

The point now is that when I use the calculation as done here

import pygmsh
import meshio
from fenics import *

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

filename_base = "Sphere-sphere-capacity_v05"

sphere_r1 =  10.0
sphere_r2 = 100.0
pot_1     =   3.0
pot_2     =   9.0

mesh       = Mesh(filename_base+'.xml')
markers    = MeshFunction("size_t", mesh, filename_base+'_physical_region.xml')
boundaries = MeshFunction('size_t', mesh, filename_base+'_facet_region.xml')
dx         = Measure('dx', domain=mesh, subdomain_data=markers)
V          = FunctionSpace(mesh, 'CG', 1)

File(filename_base+"_facetfunc.pvd").write(boundaries)

inner_boundary = DirichletBC(V, Constant(pot_1), boundaries, 1)
outer_boundary = DirichletBC(V, Constant(pot_2), boundaries, 2)
bcs =[inner_boundary, outer_boundary]

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)

… I get a warning:

*** Warning: Found no facets matching domain for boundary condition.

Note that with the three .xml files from here, I get the calculation well and correctly done.

The reason is that the boundary markers are already not in the .msh file! I obtain something like …

$MeshFormat
2.2 0 8
$EndMeshFormat
$Nodes
1844

… however, it should look similar to this, with a $PhysicalNames:

$MeshFormat
2.2 0 8
$EndMeshFormat
$PhysicalNames
3
2 1 "inner"
2 2 "outer"
3 3 "vacuum"
$EndPhysicalNames
$Nodes
5589

Is it may be this -format msh2 option for gmsh? Or is it because of the boolean operator? Hmmm …

PS, please read: I just found out that with …

geom.add_raw_code("Physical Surface(\"1\") = {1,2,3,2};") # inner
geom.add_raw_code("Physical Surface(\"2\") = {4,5,6,5};") # outer
geom.add_raw_code("Physical Volume(\"4\") = {" + sphere_final.id + "};") # volume

… so, with the number in quotes “” (e.g., \"4\") , I then obtain:

$MeshFormat
2.2 0 8
$EndMeshFormat
$PhysicalNames
3
2 1 "1"
2 2 "2"
3 3 "4"
$EndPhysicalNames
$Nodes
4669

… the desired $PhysicalNames. However, I still get the warning:

*** Warning: Found no facets matching domain for boundary condition.

Well, after all, I think it is due to the boolean operation:

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,2,3};") # inner
geom.add_raw_code("Physical Surface(\"2\") = {4,5,6};") # outer
geom.add_raw_code("Physical Volume(\"3\") = {" + sphere_final.id + "};") # volume

I subtract the small from the large sphere, which results into a sphere with a spherical ‘hole’ (this is what I expect). Then I assign the surfaces and volume.

However, Gmsh tells me now that the inner sphere surface has the same Physical surface number as the outer one (see image), which should not be … . I will see the next days … .

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