Electrostatics with Non-Uniform Charge Densities

In a previous post I demonstrated how to simulate electrostatic problems with uniform charge densities within a region, this post will generalise that to include charge densities which depend on position. This could be useful to simulate something like a particle beam, which could never really be uniformly distributed. To demonstrate this a Gaussian charge density distribution will be simulated and the resulting electric field compared with the analytical solution. Since the Gaussian distribution extends to infinity it could be useful in some applications to have the option of truncating the distribution, this won’t be demonstrated but the code will be provided in the repository.

The geometry, mesh, analysis code and outputs are available in this gitlab repository.

1 – Geometry and Mesh

The geometry and mesh in this problem are identical to that used in the post about uniform charge densities, so please see that post for detailed instructions on how to make the geometry with Gmsh and generate a dolfin mesh. I copied and pasted the files from that post into the repository for this post.

2 – FEniCS Solution

As always begin by importing the relevant packages, I’ve imported the permittivity of free space (epsilon_0) from scipy.constants

import fenics as fn
import numpy as np
import matplotlib.pyplot as plt
from scipy.constants import epsilon_0

2.1 – Import Mesh, Subdomains and Boundaries

Import the mesh, subdomains and boundaries as has been described in my previous posts.

mesh = fn.Mesh('geometry/geo_charge.xml')
subdomains = fn.MeshFunction("size_t", mesh, 'geometry/geo_charge_physical_region.xml')
boundaries = fn.MeshFunction('size_t', mesh, 'geometry/geo_charge_facet_region.xml')

2.2 – Boundary Conditions

In this case there is only one boundary condition on the electric potential at the outer edge of the model; I will assume the potential here is zero. This process has been described in my previous posts.

outer_boundary = fn.DirichletBC(V, fn.Constant(0), boundaries, 4)
bcs = [outer_boundary]

2.3 – Charge Density

The charge density distribution will be truncated at the edge of the inner circle, within that circle it will follow a 2D Gaussian distribution with surface charge density

rho = frac{Q}{2pi sigma^2} e^{-frac{r^2}{2sigma^2}}

where Q is the total charge*, sigma is the standard deviation and r is the radial coordinate; the distribution is assumed constant in the third dimension. Importantly x[0] gives the x-position and x[1] gives the y; for 3D x[2] is z.

class rho(fn.UserExpression):
    def __init__(self, markers, **kwargs):
        self.markers = markers
        super().__init__(**kwargs)
        
    def eval_cell(self, values, x, cell):
        values[0] = Q/(2*np.pi * sigma**2) * np.exp(-(x[0]**2 + x[1]**2)/2/sigma**2)
        return 0

The charge density can be visualised with the code below, the result is shown in Figure 1, by eye this distribution looks sensible.

plt.figure(figsize=(6,6))
fn.plot(mesh, linewidth=0.5)
p = fn.plot(fn.project(charge_density, V0))
plt.colorbar(p)
plt.tight_layout()
plt.show()
charge_density.png
Figure 1 – Charge density distribution.

This charge density distribution depends only on the position, but it is possible to limit this to certain subdomains with if statements. For example to have a Gaussian distribution which is truncated to the inner circle the eval_cell method would be as listed below; I will not use the truncated distribution for this example.

def eval_cell(self, values, x, cell):
    if self.markers[cell.index] == 1:
        values[0] = Q/(2*np.pi * sigma**2) * np.exp(-(x[0]**2 + x[1]**2)/2/sigma**2)
    else:
        values[0] = 0
    return 0
 

* Q is what the total charge would be if the distribution were allowed to extend to infinity.

2.4 – Solution

The solution for the electric potential is found by solving the Poisson equation, as has been done in my previous posts. The code for this is

u = fn.TrialFunction(V)
v = fn.TestFunction(V)
a = fn.dot(fn.grad(u), fn.grad(v)) * epsilon_0 * fn.dx
L = charge_density * v * fn.dx
u = fn.Function(V)
fn.solve(a == L, u, bcs)

The resulting electric potential is shown in Figure 2 and can be most easily visualised with the following code

plt.figure()
p = fn.plot(u)
plt.colorbar(p)
plt.show()
electric_potential
Figure 2 – Electric Potential

From here the electric field can be found from the gradient of the potential, it is shown in Figure 3.

electric_field = fn.project(-fn.grad(u))

plt.figure(figsize=(12,8))
fn.plot(u)
p = fn.plot(electric_field, zorder=3, cmap='inferno')
plt.colorbar(p)
plt.show()
electric_field
Figure 3 – Electric potential overlaid with the electric field.

 

2.5 – Comparison of Electric Field with Analytical Solution

A two dimensional Gaussian charge density is written as

\rho = \frac{Q}{2\pi \sigma^2} e^{-\frac{r^2}{2\sigma^2}}

where rho is the charge density. Since this distribution is rotationally symmetric, the electric field can easily be found from the integral form of Gauss’ law

E(r) = frac{Q}{2pi epsilon_0 R}left( 1 - e^{-frac{r^2}{2sigma^2}} right)

To compare this solution to the one produced by FEniCS, the electric field will be evaluated on a line along the x-axis, this can be done with FEniCS using the following code

x = np.linspace(-4, 4, 100)
yc = np.zeros(len(x))
e_field = np.array(list(map(electric_field, zip(x, yc))))

This evaluates electric_field for each value in the array of zip(x, yc) which is a list coordinates. Make a function for the analytical solution and then overlay the results on a plot, the results in Figure 4 look good by eye and Figure 5 shows there is generally better than 1% agreement.

def E(r):
    E = Q * (1-np.exp(-r**2/sigma**2/2)) / (2*np.pi*r) / epsilon_0
    return E

plt.figure(figsize=(8, 5))
plt.plot(x, e_field[:, 0], linewidth=3, label='FEniCS')
plt.plot(x, E(x), 'C1--', label='Analytical')
plt.xlabel("x-position")
plt.ylabel("Electric Field")
plt.legend(loc=0)
plt.show()
electric_field_comp
Figure 4 – Electric field along x-axis.
electric_field_difference
Figure 5 – Percentage difference between analytical and numerical results.

 

Conclusion

This short post extended the previous charge density post by allowing position dependence and thus numerical calculation of more realistic charge distributions; such as those found in particle accelerators. Doing this was just a matter of using the x parameters of the userExpression class. As with the more basic case of a uniform distribution, the agreement between analytical and numerical results is excellent.

As always the code for this example is available on gitlab. Please send me any comments, questions or corrections!

Leave a comment