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:

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