2. Capacitance extraction of a coaxial cable with adaptive meshing

2.1. Requirements

2.1.1. Software components

  • QTCAD

  • Gmsh

2.1.2. Geometry file

  • qtcad/examples/tutorials/meshes/coaxial_cable.py

2.1.3. Python script

  • qtcad/examples/tutorials/cap_coaxial_cable_adaptive.py

2.1.4. References

2.2. Briefing

This tutorial is similar to the previous tutorial (Capacitance extraction of a coaxial cable). However, here, an adaptive meshing algorithm is used to obtain the solution. In practical examples, static meshes need to be very fine in order to obtain an accurate solution. In contrast, adaptive meshing procedure refines the mesh only where needed to achieve good accuracy. Moreover, the adaptive mesh solver allows the user to specify the desired tolerance on the error.

2.3. Mesh generation

As in the previous tutorial, the mesh is generated by executing the script qtcad/examples/tutorials/meshes/coaxial_cable.py. We point out that in addition to coaxial_cable.msh the script also generates coaxial_cable.xao. The .xao file provides information about the geometry in the problem and is needed for the adaptive meshing algorithm to work. We also note that in the previous tutorial the coaxial_cable.msh file was used to obtain the final answer for the capacitance. In contrast, in this tutorial this mesh file is used as an initial mesh that is then refined to obtain a more accurate answer. For optimal results, it is recommended to make the initial mesh very coarse and allow the adaptive solver to automatically figure out how much the mesh should be refined at each position.

2.4. Setting up the device and extracting the capacitance

2.4.1. Header, input parameters, and file paths

The header, the input parameters, and directory and file paths are similar to the previous tutorial.

import qtcad.device.capacitance as cap
from qtcad.device.mesh3d import Mesh
from qtcad.device import Device
from qtcad.device import materials as mt
from qtcad.device import constants as ct

from pathlib import Path
import os
import math
from time import time
# input parameters
eps_rel = 10 # relative permittivity of the dielectric

length = 50e-3 # length of the cable
r_inner = 2e-3 # inner radius of the cable
r_outer = 5e-3 # outer radius of the cable
# directories and file paths
script_dir = Path(__file__).parent.resolve()
mesh_dir = script_dir / "meshes"  # for the mesh file
result_dir = script_dir / "output" / Path(__file__).stem  # for results
fpath_mesh = mesh_dir / "coaxial_cable.msh"  # mesh file
fpath_xao = mesh_dir / "coaxial_cable.xao"  # geometry file

# code to check that the mesh file exists
if not os.path.isfile(fpath_mesh):
   raise Exception(
      "Please run %s/coaxial_cable.py to generate the mesh and geometry files."
      % (mesh_dir)
   )

One notable difference is the definition of fpath_xao, which is the path to the raw geometry file.

2.4.2. Loading the initial mesh and defining the device

The procedure for loading the mesh and defining the device is the same as in the previous tutorial, except that the mesh produced by the qtcad/examples/tutorials/meshes/coaxial_cable.py script will now be used as the initial mesh.

# scale in the Gmsh files
scale = 1e-3  # Gmsh files are in mm

# parse the mesh
mesh = Mesh(scale, fpath_mesh)

# Create device from mesh.
dvc = Device(mesh)

# define the medium in the problem
material_properties = dict(name="arbitrary_medium", eps=eps_rel * ct.eps0)
material = mt.Material(material_properties)

# Create regions first (here only one region)
dvc.new_region("domain", material)

# Set potential to zero on the surfaces of each conductor (including ground)
dvc.new_dirichlet_bnd("gnd", 0.0)
dvc.new_dirichlet_bnd("signal", 0.0)

2.4.3. Creating and running the capacitance solver

Adaptive meshing algorithm in the capacitance solver works by gradually refining the mesh in an iterative way and terminating once the results converge within the specified tolerance.

The following code defines the solver parameters stored in a SolverParams object.

######################################################################################
# setup the solver
######################################################################################

params = cap.SolverParams()

# directory where results will be stored
params.output_dir = result_dir

# Set adaptive meshing tolerance
# Note:
#   Overall tolerance for an entry Cij is given by tol_rel * |Cij| + tol_abs.
#   Parameter tol_abs is useful when |Cij| is expected to be zero or very small.
params.tol_rel = 0.01  # relative
params.tol_abs = 0  # absolute (in F)

# Number of consecutive iterations that must agree within the tolerance
# thresholds.
params.min_converged_iters = 4 # i.e. 5 data points would be compared

# Do not terminate until min_iters refinements have been completed
params.min_iters = 8

# Save all intermediate results
params.save_intermediate_results = True

# Maximum number of CPUs to use
params.max_cpus = 8

# Name of the simulation
params.name = Path(__file__).stem

The parameter output_dir specifies the directory where results of the capacitance extraction will be stored. For example, the directory will contain the values of the capacitance matrix entries at each iteration, along with the size of the mesh.

As discussed in the comment and also in the API reference, the tolerance for an entry \(C_{ij}\) is given by tol_rel × \(C_{ij}\) + tol_abs. Typically, tol_abs can be set to zero. tol_abs is useful when the capacitance is expected to be zero or very small.

Parameter min_converged_iters specifies how many consecutive iterations must agree within the tolerance thresholds. For example, if min_converged_iters were set to 1, the solver would terminate when the last two meshes produced similar results. We recommend setting the values of this parameter at least as high as the default value in order to avoid false convergence, i.e., situations in which the adaptive algorithm stops before the solution is found with an accuracy that correctly matches the specified tolerance.

Parameter min_iters can be used to prevent the solver from terminating before the given number of refinements has been completed. It is useful in rare cases where the error appears to have converged before the mesh is sufficiently refined. To avoid this issue, it is a good practice to study the final refined mesh and ensure that it is resolves all of important features in the device geometry. If the simulation suffers from the aforementioned false convevergence issues, min_iters can be used to force the solver not to terminate too early.

Parameter save_intermediate_results specifies which files are to be updated at every iteration, as opposed to at the end of the solver execution. In particular, if save_intermediate_results is True those files will include the refined mesh file. It is useful to set this parameter to True if there is a possibility that the solver may need to be interrupted during the execution.

Parameter max_cpus can be used to limit the number of logical CPU cores that will be used by the solver. For example, this could be useful to avoid CPU oversubscription when multiple QTCAD simulations are carried out in parallel.

Lastly, parameter name will be used when saving iteration results to files and can be helpful for differenting the results of different simulations.

The signal conductors are defined in the same way as in the previous tutorial.

# Set up signal conductors
signal_conductors = ["signal"]

The solver is then initialized using the parameters defined above.

# initialize the solver
cap_solver = cap.Solver(dvc, signal_conductors, params, geo_file=fpath_xao)

Four arguments are used here when instantiating the Solver class. The first argument is the device object, the second argument is a list of signal conductors, the third argument is the SolverParams object, and the fourth argument is the geometry file. The geometry file serves two purposes: (1) it indicates to the solver that adaptive meshing should be used and (2) it provides the geometry information needed for the adaptive solver to work.

The following code executes the adaptive solver algorithm:

# execute the solver algorithm
t0 = time()
c = cap_solver.solve()
dt = time() - t0
print("Time taken: %.1f s" % dt)

When the solver begins execution, it displays the path to files where one can observe the solver convergence in real time:

Convergence data will be saved in:
     [params.output_dir]\cap_coaxial_cable_adaptive_values.txt
     [params.output_dir]\cap_coaxial_cable_adaptive_delta.txt
     [params.output_dir]\cap_coaxial_cable_adaptive_delta_rel.txt

In cap_coaxial_cable_adaptive_values.txt, one would find the elements of the capacitance matrix computed at each iteration, along with the size of mesh:

Values of the capacitance at each iteration

The file denoted as “_delta” shows the discrepancy in farads among the last min_converged_iterations steps of the solver. The file denoted as “_delta_rel”, shows the corresponding relative errors. When those relative errors get below the threshold params.tol_rel and the min_iters condition is satisfied, the solver terminates.

The final results can be displayed in the same way as the results of the static solver.

# print the capacitance
print(
   "Capacitance between the signal and ground lines: %.2f pF"
   % (c["signal", "signal"] * 1e12)
)

# compute and print the theoretical value
# See Pozar, David M. "Microwave engineering." Fourth Edition, John Wiley & Sons, Inc (2012).
theor_value = 2 * math.pi * material.eps * length / math.log(r_outer / r_inner)
print("Theoretical value of capacitance is            : %.2f pF" % (theor_value * 1e12))

The resulting values are shown below:

Capacitance between the signal and ground lines: 30.12 pF
Theoretical value of capacitance is            : 30.36 pF

Note

The final capacitance values is very close to the value obtained on a coarse mesh with the static solver. While it may appear that the static solver was more efficient, this is a very specific case, and the static solver does not offer a general way to estimate the error. In general problems in which the theoretical value is not known, employing the adaptive solver is a much more reliable and systematic approach.

2.5. Full code

__copyright__ = "Copyright 2022-2025, Nanoacademic Technologies Inc."

import qtcad.device.capacitance as cap
from qtcad.device.mesh3d import Mesh
from qtcad.device import Device
from qtcad.device import materials as mt
from qtcad.device import constants as ct
from pathlib import Path
import os
import math
from time import time

# input parameters
eps_rel = 10 # relative permittivity of the dielectric

length = 50e-3 # length of the cable
r_inner = 2e-3 # inner radius of the cable
r_outer = 5e-3 # outer radius of the cable

# directories and file paths
script_dir = Path(__file__).parent.resolve()
mesh_dir = script_dir / "meshes"  # for the mesh file
result_dir = script_dir / "output" / Path(__file__).stem  # for results
fpath_mesh = mesh_dir / "coaxial_cable.msh"  # mesh file
fpath_xao = mesh_dir / "coaxial_cable.xao"  # geometry file

# code to check that the mesh file exists
if not os.path.isfile(fpath_mesh):
   raise Exception(
      "Please run %s/coaxial_cable.py to generate the mesh and geometry files."
      % (mesh_dir)
   )

# scale in the Gmsh files
scale = 1e-3  # Gmsh files are in mm

# parse the mesh
mesh = Mesh(scale, fpath_mesh)

# Create device from mesh.
dvc = Device(mesh)

# define the medium in the problem
material_properties = dict(name="arbitrary_medium", eps=eps_rel * ct.eps0)
material = mt.Material(material_properties)

# Create regions first (here only one region)
dvc.new_region("domain", material)

# Set potential to zero on the surfaces of each conductor (including ground)
dvc.new_dirichlet_bnd("gnd", 0.0)
dvc.new_dirichlet_bnd("signal", 0.0)


######################################################################################
# setup the solver
######################################################################################

params = cap.SolverParams()

# directory where results will be stored
params.output_dir = result_dir

# Set adaptive meshing tolerance
# Note:
#   Overall tolerance for an entry Cij is given by tol_rel * |Cij| + tol_abs.
#   Parameter tol_abs is useful when |Cij| is expected to be zero or very small.
params.tol_rel = 0.01  # relative
params.tol_abs = 0  # absolute (in F)

# Number of consecutive iterations that must agree within the tolerance
# thresholds.
params.min_converged_iters = 4 # i.e. 5 data points would be compared

# Do not terminate until min_iters refinements have been completed
params.min_iters = 8

# Save all intermediate results
params.save_intermediate_results = True

# Maximum number of CPUs to use
params.max_cpus = 8

# Name of the simulation
params.name = Path(__file__).stem

# Set up signal conductors
signal_conductors = ["signal"]

# initialize the solver
cap_solver = cap.Solver(dvc, signal_conductors, params, geo_file=fpath_xao)

# execute the solver algorithm
t0 = time()
c = cap_solver.solve()
dt = time() - t0
print("Time taken: %.1f s" % dt)

# print the capacitance
print(
   "Capacitance between the signal and ground lines: %.2f pF"
   % (c["signal", "signal"] * 1e12)
)

# compute and print the theoretical value
# See Pozar, David M. "Microwave engineering." Fourth Edition, John Wiley & Sons, Inc (2012).
theor_value = 2 * math.pi * material.eps * length / math.log(r_outer / r_inner)
print("Theoretical value of capacitance is            : %.2f pF" % (theor_value * 1e12))