4. Maxwell eigenmode extraction of a coplanar waveguide resonator with adaptive meshing
4.1. Requirements
4.1.1. Software components
QTCAD
Gmsh
4.1.2. Geometry file
qtcad/examples/tutorials/meshes/meandered_resonator.py
4.1.3. Python script
qtcad/examples/tutorials/maxwell_meandered_resonator.py
4.1.4. References
4.2. Briefing
In this tutorial, we extract the first two Maxwell eigenmodes of a quarter-wavelength meandered coplanar waveguide resonator. The resonator is formed by a ground plane and a central strip, placed on a silicon substrate. Several conductor crossovers are placed along the strip to connect the ground plane on both of its sides as to avoid spurious modes [1]. Both the ground plane and the strip are modelled as sheets made of a perfect electric conductor (PEC). The whole structure is enclosed inside a cavity.
4.3. Geometry of the problem
The geometry of the device is shown in the figure below:
The following table shows the physical groups set up in the geometry file:
Gmsh physical group |
Dimension |
Purpose |
|---|---|---|
|
2D |
The ground plane, including its conductor crossovers |
|
2D |
The central strip of the coplanar waveguide |
|
3D |
The silicon substrate on which the ground plane and strip lie |
|
2D |
The inner surface of the cavity enclosing the structure [2] |
|
3D |
The air that fills the space in the cavity above the silicon substrate |
4.4. Setting up the device and finding the Maxwell eigenmodes
The procedure for finding the Maxwell eigenmodes is similar to that for the capacitance
solver (see, for example, Capacitance extraction of a coaxial cable with adaptive meshing).
You may also find it useful to look at Maxwell Eigenmodes and the API
reference for the maxwell_eigenmode module.
4.4.1. Header, input parameters, and file paths
"""
Find the first two modes of a meandered quarter-wavelength coplanar waveguide resonator
inside a cavity.
"""
from pathlib import Path
import os
from time import time
import numpy as np
# Import the relevant modules of QTCAD.
from qtcad.device.maxwell_eigenmode import Solver
from qtcad.device.maxwell_eigenmode import SolverParams
from qtcad.device.device import Device
from qtcad.device.mesh3d import Mesh
from qtcad.device import materials as mt
from qtcad.device import constants as ct
Similarly to capacitance, the
maxwell_eigenmode module contains
Solver and
SolverParams classes.
The Solver
class contains algorithms needed to find the Maxwell eigenmodes and the
SolverParams
class can be used to control parameters of the solver.
# Scale in the Gmsh files.
scale = 1e-6
# The total length of the resonator in meters.
length = 5950 * scale
# Directories and file paths.
script_dir = Path(__file__).parent.resolve()
# For mesh and raw geometry files.
input_dir = script_dir / "meshes"
# For results.
result_dir = script_dir / "output" / Path(__file__).stem
# Mesh file.
fpath_mesh = input_dir / "meandered_resonator.msh4"
# Raw geometry file.
fpath_xao = input_dir / "meandered_resonator.xao"
# Check if the mesh and raw geometry files exist.
if not os.path.isfile(fpath_mesh) or not os.path.isfile(fpath_xao):
raise Exception(
"Please run %s/meandered_resonator.py to generate the mesh and raw geometry files."
% (input_dir)
)
4.4.2. Loading the initial mesh and defining the device
As in the case of the capacitance solver, Capacitance extraction of a coaxial cable with adaptive meshing, we first parse the mesh file, initialize the device,
######################################################################################
# Set up the device.
######################################################################################
# Parse the mesh and initialize the device.
mesh = Mesh(scale, fpath_mesh)
dvc = Device(mesh)
and assign the media to 3D physical groups and boundary conditions to 2D physical groups.
material_sub = mt.Si
material_air = mt.vacuum
# Assign media to regions.
dvc.new_region("substrate", material_sub)
dvc.new_region("air", material_air)
# Assign perfect electric conductor boundary condition to all conductors.
dvc.new_pec_bnd("gnd")
dvc.new_pec_bnd("strip")
dvc.new_pec_bnd("envelope")
In the fourth and fifth line in the block above, we assign silicon to "substrate"
and vacuum to "air" [3].
Next, we assign the perfect electric conductor (PEC) boundary condition to the
conducting surfaces "gnd" and "strip" and to the inner surface of the cavity
"envelope".
See Boundary conditions for more details on the boundary conditions in
the Maxwell eigensolver.
4.4.3. Creating and running the Maxwell eigenmode solver
Next, we define the parameters of the solver by creating a
SolverParams
object and modifying its attributes.
######################################################################################
# Set up the solver.
######################################################################################
params = SolverParams()
# Number of modes to find.
params.num_modes = 2
# Adaptive meshing (relative) tolerance on the frequency.
params.tol_rel = 0.05
# Directory where output files will be stored.
params.output_dir = result_dir
# Uncomment the line below in order to save all intermediate results at every iteration.
# params.save_intermediate_results = True
# Number of consecutive iterations that must agree within the tolerance thresholds.
# Uncomment the line below to change the default behaviour.
# Then, for each mode, the last result will be compared to 7 previous data points (i.e.
# 8 data points in total are compared for each mode).
# params.min_converged_iters = 7
# Do not terminate until the refined mesh contains at least min_nodes nodes.
params.min_nodes = 20000
In the code above, num_modes is the number of modes to be found.
Parameter output_dir specifies the directory where the solver results will be saved,
similarly to the capacitance solver.
Parameter tol_rel is the relative tolerance on the frequencies.
Other useful parameters include save_intermediate_results and
min_converged_iters.
The former allows saving all intermediate results to the output_dir directory and is
useful if the execution of the solver needs to be interrupted.
The parameter min_converged_iters indicates how many of the last iterations are used
to compute the current error in the eigenmode frequencies.
It can be increased for improved robustness, but this would lead to a longer simulation
time.
Parameter min_nodes can be used to prevent the solver from terminating before the
given number of nodes has been reached.
It is useful in 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 convergence issues,
min_nodes can be used to force the solver not to terminate too early.
See Studying the refined mesh for the comparison of the refined meshes with
and without using this parameter.
Next, we initialize and execute the solver with the parameters params created above.
######################################################################################
# Run the solver (results are stored in the folder specified by params.output_dir).
######################################################################################
slv = Solver(dvc, params, geo_file=fpath_xao)
t0 = time()
slv.solve()
dt = time() - t0
print("Solution completed in %.2f s" % dt)
The frequencies are stored in dvc.maxwell_freqs:
frequencies_ghz = [f"{f / 1e9:.3f}" for f in dvc.maxwell_freqs]
print("Frequencies found (GHz): " + ", ".join(frequencies_ghz))
which results in
Frequencies found (GHz): 4.734, 14.389
Similarly to the capacitance solver, the input
geo_file is needed in order to use adaptive meshing solver instead of the static
meshing.
The mesh created by qtcad/examples/tutorials/meshes/meandered_resonator.py will then
be used as the initial mesh.
The solver will display the following lines:
Convergence data will be saved in:
[params.output_dir]\meandered_resonator-values.txt
[params.output_dir]\meandered_resonator-delta.txt
[params.output_dir]\meandered_resonator-delta_rel.txt
indicating the paths to files where one will find
mode frequencies at each iteration,
the corresponding mesh sizes,
timing information for each iteration and cumulative time since the start of the solver execution.
4.4.4. Studying the refined mesh
The refined mesh can be viewed via the following command:
gmsh [params.output_dir]\meandered_resonator-refined-mesh.msh4
The mesh is shown below for two simulations [4]: with and without the line
params.min_nodes = 20000.
Fig. 4.4.5 Refined mesh with the default params.min_nodes.
Fig. 4.4.6 Refined mesh with params.min_nodes = 20000.
Setting the parameter min_nodes to 20000, we obtain a mesh with better refinement of
the gap between the strip and the ground of the coplanar waveguide.
We remark that the desired refinement strongly depends on the accuracy required for the
particular application.
4.4.5. Extracting further solver results
In addition to the information in the convergence files, one can access the solver
results via the Device object dvc.
With the mode frequencies contained in the attribute maxwell_freqs, let us compare
the final computed frequencies to theoretical results in the following.
######################################################################################
# Extract additional information from the final results.
######################################################################################
# Find the theoretical value
#
# NOTE:
# The effective permittivity is computed for a coplanar waveguide placed on an
# infinitely thick and wide substrate.
#
# Simons, Rainee N. Coplanar waveguide circuits, components, and systems.
# John Wiley & Sons, 2001.
#
# Effects of the finite substrate size or the cavity are neglected due to the large
# distance between the resonator and the cavity (and, consequently, also the edges of
# the substrate).
# Please refer to Simons (2001) for details on the treatment of such effects.
# It should also be noted that the theoretical value does not take into account the
# meandered layout of the resonator.
eps_eff = (material_air.eps + material_sub.eps) / 2
wavelength = length * 4
vphase = 1 / np.sqrt(eps_eff * ct.mu0)
freq_theor = vphase / wavelength
print(f"Frequency of the fundamental mode (analytical): {freq_theor / 1e9:.3f} GHz")
# Display the frequency of the fundamental mode and compare to theoretical value.
err = abs(dvc.maxwell_freqs[0] - freq_theor) / freq_theor
print(
"\nFrequency of the fundamental mode (computed): %.3f GHz (rel. err: %.3f)"
% (dvc.maxwell_freqs[0] / 1e9, err)
)
# For a quarter wavelength resonator, the resonances should occur at odd multiples of
# its fundamental frequency. For the first two modes, their ratio should be equal to 3.
freq_ratio = dvc.maxwell_freqs[1] / dvc.maxwell_freqs[0]
freq_ratio_theor = 3
err_freq_ratio = abs(freq_ratio - freq_ratio_theor) / freq_ratio_theor
print(
f"\nRatio between the first excited and fundamental mode (computed): {freq_ratio:.3f}"
f"\nRelative error from the expected ratio of {freq_ratio_theor}: {err_freq_ratio:.3f}"
)
The results are shown below:
Frequency of the fundamental mode (analytical): 4.979 GHz
Frequency of the fundamental mode (computed): 4.734 GHz (rel. err: 0.049)
Ratio between the first excited and fundamental mode (computed): 3.039
Relative error from the expected ratio of 3: 0.013
Also, methods
get_e_field_maxwell
and get_b_field_maxwell can be
used to find the electric field and magnetic flux density for each of the computed
modes.
# Get the electric and magnetic fields.
e_field = dvc.get_e_field_maxwell()
b_field = dvc.get_b_field_maxwell()
The code below shows an example of how the resulting fields can be used to extract information about the energy stored in different parts of the device.
# Fields in the fundamental mode.
e_field_0 = e_field[..., 0]
b_field_0 = b_field[..., 0]
# Find the ratio between the energy stored in the substrate and the energy in the air.
energy_sub = dvc.energy_e(e_field_0, "substrate") + dvc.energy_b(b_field_0, "substrate")
energy_air = dvc.energy_e(e_field_0, "air") + dvc.energy_b(b_field_0, "air")
print(
"\nIn the fundamental mode, the ratio of energy in the substrate and in the air is"
f" {energy_sub / energy_air:.1f}:1"
)
The code produces the following result:
In the fundamental mode, the ratio of energy in the substrate and in the air is 2.4:1
In the params.output_dir directory, the solver also saves .vtu files, which can
be opened in ParaView, containing the electromagnetic field for each mode.
Below we show the results for the fundamental mode, which has the index 0 in this
tutorial:
Fig. 4.4.7 Magnitude of the electric field in the fundamental mode. A clip filter at \(z=0\) is used to visualize the field in the gap between the ground plane and the central strip of the resonator.
Fig. 4.4.8 Magnitude of the magnetic flux density in the fundamental mode. A clip filter at \(z=0\) is used to visualize the field in the gap between the ground plane and the central strip of the resonator.
4.4.6. Effects of the absence of conductor crossovers
Lastly, if we modify the number of conductor crossovers on each straight section of the
waveguide when generating the mesh with
qtcad/examples/tutorials/meshes/meandered_resonator.py from 2 to 0, the second mode
computed will not be associated to the resonator, but to an unwanted slotline mode [1]
with frequency 11.905 GHz.
This is 2.491 times the frequency of the first mode associated to the resonator computed
in this different design [5], which shows that this second mode is not related to an
excitation of our quarter-wavelength resonator.
This is clearly seen when we look at the magnitudes of the electric field in each case:
Fig. 4.4.9 Magnitude of the electric field in the second mode when conductor crossovers are present in the design: this is the first excited mode of the resonator. A clip filter at \(z=0\) is used to visualize the field in the gap between the ground plane and the central strip of the resonator.
Fig. 4.4.10 Magnitude of the electric field in the second mode when no conductor crossovers are present in the design: this is an unwanted slotline mode. A clip filter at \(z=0\) is used to visualize the field in the ground-plane close to the central strip of the resonator.
4.5. Full code
__copyright__ = "Copyright 2022-2025, Nanoacademic Technologies Inc."
"""
Find the first two modes of a meandered quarter-wavelength coplanar waveguide resonator
inside a cavity.
"""
from pathlib import Path
import os
from time import time
import numpy as np
# Import the relevant modules of QTCAD.
from qtcad.device.maxwell_eigenmode import Solver
from qtcad.device.maxwell_eigenmode import SolverParams
from qtcad.device.device import Device
from qtcad.device.mesh3d import Mesh
from qtcad.device import materials as mt
from qtcad.device import constants as ct
# Scale in the Gmsh files.
scale = 1e-6
# The total length of the resonator in meters.
length = 5950 * scale
# Directories and file paths.
script_dir = Path(__file__).parent.resolve()
# For mesh and raw geometry files.
input_dir = script_dir / "meshes"
# For results.
result_dir = script_dir / "output" / Path(__file__).stem
# Mesh file.
fpath_mesh = input_dir / "meandered_resonator.msh4"
# Raw geometry file.
fpath_xao = input_dir / "meandered_resonator.xao"
# Check if the mesh and raw geometry files exist.
if not os.path.isfile(fpath_mesh) or not os.path.isfile(fpath_xao):
raise Exception(
"Please run %s/meandered_resonator.py to generate the mesh and raw geometry files."
% (input_dir)
)
######################################################################################
# Set up the device.
######################################################################################
# Parse the mesh and initialize the device.
mesh = Mesh(scale, fpath_mesh)
dvc = Device(mesh)
material_sub = mt.Si
material_air = mt.vacuum
# Assign media to regions.
dvc.new_region("substrate", material_sub)
dvc.new_region("air", material_air)
# Assign perfect electric conductor boundary condition to all conductors.
dvc.new_pec_bnd("gnd")
dvc.new_pec_bnd("strip")
dvc.new_pec_bnd("envelope")
######################################################################################
# Set up the solver.
######################################################################################
params = SolverParams()
# Number of modes to find.
params.num_modes = 2
# Adaptive meshing (relative) tolerance on the frequency.
params.tol_rel = 0.05
# Directory where output files will be stored.
params.output_dir = result_dir
# Uncomment the line below in order to save all intermediate results at every iteration.
# params.save_intermediate_results = True
# Number of consecutive iterations that must agree within the tolerance thresholds.
# Uncomment the line below to change the default behaviour.
# Then, for each mode, the last result will be compared to 7 previous data points (i.e.
# 8 data points in total are compared for each mode).
# params.min_converged_iters = 7
# Do not terminate until the refined mesh contains at least min_nodes nodes.
params.min_nodes = 20000
######################################################################################
# Run the solver (results are stored in the folder specified by params.output_dir).
######################################################################################
slv = Solver(dvc, params, geo_file=fpath_xao)
t0 = time()
slv.solve()
dt = time() - t0
print("Solution completed in %.2f s" % dt)
frequencies_ghz = [f"{f / 1e9:.3f}" for f in dvc.maxwell_freqs]
print("Frequencies found (GHz): " + ", ".join(frequencies_ghz))
######################################################################################
# Extract additional information from the final results.
######################################################################################
# Find the theoretical value
#
# NOTE:
# The effective permittivity is computed for a coplanar waveguide placed on an
# infinitely thick and wide substrate.
#
# Simons, Rainee N. Coplanar waveguide circuits, components, and systems.
# John Wiley & Sons, 2001.
#
# Effects of the finite substrate size or the cavity are neglected due to the large
# distance between the resonator and the cavity (and, consequently, also the edges of
# the substrate).
# Please refer to Simons (2001) for details on the treatment of such effects.
# It should also be noted that the theoretical value does not take into account the
# meandered layout of the resonator.
eps_eff = (material_air.eps + material_sub.eps) / 2
wavelength = length * 4
vphase = 1 / np.sqrt(eps_eff * ct.mu0)
freq_theor = vphase / wavelength
print(f"Frequency of the fundamental mode (analytical): {freq_theor / 1e9:.3f} GHz")
# Display the frequency of the fundamental mode and compare to theoretical value.
err = abs(dvc.maxwell_freqs[0] - freq_theor) / freq_theor
print(
"\nFrequency of the fundamental mode (computed): %.3f GHz (rel. err: %.3f)"
% (dvc.maxwell_freqs[0] / 1e9, err)
)
# For a quarter wavelength resonator, the resonances should occur at odd multiples of
# its fundamental frequency. For the first two modes, their ratio should be equal to 3.
freq_ratio = dvc.maxwell_freqs[1] / dvc.maxwell_freqs[0]
freq_ratio_theor = 3
err_freq_ratio = abs(freq_ratio - freq_ratio_theor) / freq_ratio_theor
print(
f"\nRatio between the first excited and fundamental mode (computed): {freq_ratio:.3f}"
f"\nRelative error from the expected ratio of {freq_ratio_theor}: {err_freq_ratio:.3f}"
)
# Get the electric and magnetic fields.
e_field = dvc.get_e_field_maxwell()
b_field = dvc.get_b_field_maxwell()
# Fields in the fundamental mode.
e_field_0 = e_field[..., 0]
b_field_0 = b_field[..., 0]
# Find the ratio between the energy stored in the substrate and the energy in the air.
energy_sub = dvc.energy_e(e_field_0, "substrate") + dvc.energy_b(b_field_0, "substrate")
energy_air = dvc.energy_e(e_field_0, "air") + dvc.energy_b(b_field_0, "air")
print(
"\nIn the fundamental mode, the ratio of energy in the substrate and in the air is"
f" {energy_sub / energy_air:.1f}:1"
)