HFSS Driven Modal Simulation of an Interdigital Capacitor#
This notebook demonstrates how to set up and run a driven modal (S-parameter) simulation of an interdigital capacitor using PyAEDT (Ansys HFSS Python interface).
Driven modal analysis computes scattering parameters (S-parameters) of structures with ports, enabling characterization of coupling capacitance, insertion loss, and frequency-dependent behavior.
Prerequisites:
Ansys HFSS installed (requires license)
Install hfss extras:
uv sync --extra hfssorpip install qpdk[hfss]
References:
PyAEDT Documentation: https://aedt.docs.pyansys.com/
HFSS Driven Modal Examples: https://examples.aedt.docs.pyansys.com/
Interdigital Capacitor Theory: [LeiZhuW00]
Setup and Imports#
Create an Interdigital Capacitor Component#
We’ll use QPDK’s interdigital capacitor cell, which creates interleaved metal fingers for distributed capacitance.
import gdsfactory as gf
import numpy as np
from qpdk import PDK
from qpdk.cells.capacitor import interdigital_capacitor
from qpdk.cells.waveguides import straight_open
from qpdk.models.capacitor import interdigital_capacitor_capacitance_analytical
from qpdk.models.cpw import cpw_ep_r_from_cross_section
from qpdk.simulation import HFSS, Q3D, prepare_component_for_aedt
from qpdk.tech import coplanar_waveguide
PDK.activate()
# Create an interdigital capacitor
# This design has 6 fingers with 20µm finger length
cpw_width, cpw_gap = 10, 6
cross_section = coplanar_waveguide(width=cpw_width, gap=cpw_gap)
idc_component = interdigital_capacitor(
fingers=6, # Number of interleaved fingers
finger_length=20.0, # Length of each finger in µm
finger_gap=2.0, # Gap between adjacent fingers in µm
thickness=5.0, # Finger width in µm
cross_section=cross_section,
)
# Attach straight open waveguides to the ports to provide a feedline
# for the lumped ports in HFSS.
c = gf.Component(name="idc_with_feeds")
idc_ref = c << idc_component
open_wvg = straight_open(length=5, cross_section=cross_section)
# Connect feedlines to both ports
feed1 = c << open_wvg
feed1.connect("o1", idc_ref.ports["o1"])
c.add_port("o1", port=feed1.ports["o2"])
feed2 = c << open_wvg
feed2.connect("o1", idc_ref.ports["o2"])
c.add_port("o2", port=feed2.ports["o2"])
# Use the combined component for the rest of the notebook
idc_component = c
# Visualize the component
idc_component.show()
print(f"Interdigital capacitor bounding box: {idc_component.bbox}")
print(f"Number of ports: {len(idc_component.ports)}")
for port in idc_component.ports:
print(f" {port.name}: center={port.center}, orientation={port.orientation}°")
Estimate Capacitance#
Before running the full-wave simulation, we can estimate the mutual capacitance using the analytical conformal mapping model for interdigital capacitors [ID04].
For a structure with \(n\) fingers of width \(w\), gap \(g\), and overlap length \(L\), the metallization ratio is \(\eta = \frac{w}{w + g}\). The interior and exterior capacitances per unit length are derived using the complete elliptic integrals of the first kind \(K(k)\):
The total mutual capacitance for \(n\) fingers is then:
# Get substrate permittivity from cross-section
ep_r = cpw_ep_r_from_cross_section(cross_section)
# Analytical estimate using QPDK model
C_estimate = interdigital_capacitor_capacitance_analytical(
fingers=6,
finger_length=20.0,
finger_gap=2.0,
thickness=5.0,
ep_r=float(ep_r),
)
print(f"Estimated capacitance: {float(C_estimate) * 1e15:.2f} fF")
Initialize HFSS Project#
Set up an HFSS project for driven modal analysis with ports.
Note: This section requires Ansys HFSS to be installed and licensed.
# Configuration for HFSS simulation
HFSS_CONFIG = {
"solution_frequency_ghz": 5.0, # Adaptive mesh at 5 GHz
"sweep_start_ghz": 0.1, # Sweep from 100 MHz
"sweep_stop_ghz": 20.0, # to 20 GHz
"sweep_points": 401, # Number of frequency points
"max_passes": 16,
"max_delta_s": 0.002, # 0.2% S-parameter convergence
}
Build HFSS Model (Example Code)#
The following demonstrates the complete workflow for driven modal simulation:
Create HFSS project with “DrivenModal” solution type
Build capacitor geometry with ports
Configure frequency sweep
Run simulation and extract S-parameters
Note
This code requires Ansys HFSS. The structure below shows the complete workflow.
# Example HFSS driven modal simulation workflow
# This code block demonstrates the full workflow but requires HFSS license
import os # noqa: E402
# Ensure Ansys path is set so PyAEDT can find it
ansys_default_path = "/usr/ansys_inc/v252/AnsysEM"
if "ANSYSEM_ROOT252" not in os.environ and Path(ansys_default_path).exists():
os.environ["ANSYSEM_ROOT252"] = ansys_default_path
from ansys.aedt.core import Hfss, settings # noqa: E402
settings.use_grpc_uds = False
# Create temporary directory for project
temp_dir = tempfile.TemporaryDirectory(suffix=".ansys_qpdk")
project_path = Path(temp_dir.name) / "idc_driven.aedt"
# Initialize HFSS with Driven Modal solution
hfss = Hfss(
project=str(project_path),
design="InterdigitalCapacitor",
solution_type="DrivenModal",
non_graphical=False,
new_desktop=True,
version="2025.2",
)
hfss.modeler.model_units = "um"
print(f"HFSS project created: {hfss.project_file}")
print(f"Design name: {hfss.design_name}")
print(f"Solution type: {hfss.solution_type}")
Build Interdigital Capacitor Geometry#
Import the gdsfactory component geometry into HFSS using native GDS import.
This uses Hfss.import_gds_3d which automatically handles 3D layer mapping.
# Prepare component for export
prepared_component = prepare_component_for_aedt(idc_component, margin_draw=50)
# Initialize HFSS wrapper
hfss_sim = HFSS(hfss)
# Import the component geometry using native GDS import
success = hfss_sim.import_component(prepared_component, import_as_sheets=True)
print(f"GDS import successful: {success}")
# Add substrate below the component
substrate_name = hfss_sim.add_substrate(
prepared_component,
thickness=500.0,
material="silicon",
)
print(f"Created substrate: {substrate_name}")
# Add air region for driven simulation
air_region_name = hfss_sim.add_air_region(
prepared_component,
height=500.0,
substrate_thickness=500.0,
pec_boundary=False,
)
print(f"Created air region: {air_region_name}")
# Assign radiation boundary to outer faces for driven analysis
hfss.assign_radiation_boundary_to_objects(air_region_name)
print("Assigned radiation boundary to air region")
Create Lumped Ports#
Add lumped ports at both ends of the capacitor to measure S-parameters. The ports are placed at the CPW feed locations.
print("Creating lumped ports.")
hfss_sim.add_lumped_ports(prepared_component.ports, cpw_gap, cpw_width)
for port in prepared_component.ports:
print(
f" Created {port.name} at {port.center} with orientation {port.orientation}°"
)
Configure Driven Modal Analysis#
Set up the solution with frequency sweep to compute S-parameters across the desired frequency range.
# Create driven modal setup
setup = hfss.create_setup(
name="DrivenSetup",
Frequency=f"{HFSS_CONFIG['solution_frequency_ghz']}GHz",
)
setup.props["MaxDeltaS"] = HFSS_CONFIG["max_delta_s"]
setup.props["MaximumPasses"] = HFSS_CONFIG["max_passes"]
setup.props["MinimumPasses"] = 2
setup.props["PercentRefinement"] = 30
setup.update()
# Create frequency sweep
sweep = setup.create_frequency_sweep(
unit="GHz",
name="FrequencySweep",
start_frequency=HFSS_CONFIG["sweep_start_ghz"],
stop_frequency=HFSS_CONFIG["sweep_stop_ghz"],
sweep_type="Interpolating",
num_of_freq_points=HFSS_CONFIG["sweep_points"],
)
print("Driven modal setup configured:")
print(f" - Solution frequency: {HFSS_CONFIG['solution_frequency_ghz']} GHz")
print(
f" - Sweep range: {HFSS_CONFIG['sweep_start_ghz']} - {HFSS_CONFIG['sweep_stop_ghz']} GHz"
)
print(f" - Number of points: {HFSS_CONFIG['sweep_points']}")
Run Simulation#
Execute the driven modal analysis with frequency sweep.
print("Starting driven modal analysis...")
print("(This may take several minutes)")
# Save project before analysis
hfss.save_project()
# Run the analysis
start_time = time.time()
success = hfss.analyze_setup("DrivenSetup", cores=4)
elapsed = time.time() - start_time
if not success:
print("\nERROR: HFSS simulation failed!")
else:
print(f"Analysis completed in {elapsed:.1f} seconds")
Extract and Plot S-Parameters#
Get the S-parameters from the simulation and visualize the results.
import matplotlib.pyplot as plt # noqa: E402
# Extract results using the wrapper
df_results = hfss_sim.get_sparameter_results(setup.name, sweep.name)
frequencies_ghz = df_results["frequency_ghz"].to_numpy()
# Plot S-parameters
fig, axes = plt.subplots(2, 1, figsize=(10, 8))
# Filter for S11 and S21 type traces
s11_col = next(col for col in df_results.columns if "S(o1:1,o1:1)" in col)
s21_col = next(col for col in df_results.columns if "S(o2:1,o1:1)" in col)
s11_trace = df_results[s11_col].to_numpy().astype(np.complex128)
s21_trace = df_results[s21_col].to_numpy().astype(np.complex128)
s11_mag_db = 20 * np.log10(np.abs(s11_trace))
s21_mag_db = 20 * np.log10(np.abs(s21_trace))
axes[0].plot(frequencies_ghz, s11_mag_db, label=r"|S_{11}|")
axes[1].plot(frequencies_ghz, s21_mag_db, label=r"|S_{21}|")
axes[0].set_xlabel("Frequency (GHz)")
axes[0].set_ylabel("Magnitude (dB)")
axes[0].set_title("Return Loss ($S_{11}$)")
axes[0].grid(True)
axes[0].legend()
axes[1].set_xlabel("Frequency (GHz)")
axes[1].set_ylabel("Magnitude (dB)")
axes[1].set_title("Insertion Loss ($S_{21}$)")
axes[1].grid(True)
axes[1].legend()
plt.tight_layout()
# plt.show()
Extract Capacitance from Admittance (Y-Parameters)#
Relying only on the magnitude of \(S_{21}\) assumes the geometry behaves identically to a single perfect capacitor floating in a vacuum. In reality, the structure has shunt parasitic capacitances to ground (\(C_{11}\) and \(C_{22}\)) that skew the \(S_{21}\) magnitude.
The robust way to extract mutual capacitance is using Y-parameters (Admittance), see [MP12]. In a Pi-network model, the mutual admittance \(Y_{12}\) isolates the series element:
Therefore, the exact mutual capacitance is:
# Extract Y21 parameter manually using PyAEDT's solution data
solution_y = hfss.post.get_solution_data(
expressions="Y(o2:1,o1:1)", setup_sweep_name=f"{setup.name} : {sweep.name}"
)
# Parse complex Y-parameters
_, y_real = solution_y.get_expression_data(formula="real")
_, y_imag = solution_y.get_expression_data(formula="imag")
y21_trace = np.array(y_real) + 1j * np.array(y_imag)
frequencies_ghz_y = np.array(solution_y.primary_sweep_values)
# Analysis frequencies in GHz
analysis_frequencies_ghz = [1, 5, 10]
print("\n=== HFSS Capacitance Analysis ===")
print("-" * 40)
print(f"Analytical estimate: {C_estimate * 1e15:.2f} fF")
print("-" * 40)
C_hfss_values = {}
for freq_target in analysis_frequencies_ghz:
idx = np.argmin(np.abs(frequencies_ghz_y - freq_target))
freq_hz = frequencies_ghz_y[idx] * 1e9
y21 = y21_trace[idx]
ω = 2 * np.pi * freq_hz
C_extracted = -np.imag(y21) / ω
C_hfss_values[freq_target] = C_extracted
print(
f"At {freq_hz / 1e9:.2f} GHz: Y21 = {y21:.2e}, C ≈ {C_extracted * 1e15:.2f} fF"
)
print(
f"Relative difference: {(float(C_estimate) - C_extracted) / float(C_estimate) * 100:.2f}%"
)
HFSS Cleanup#
Close HFSS and clean up temporary files before starting Q3D.
# Save and close HFSS
hfss.save_project()
# hfss.release_desktop()
time.sleep(2)
# Clean up temp directory
temp_dir.cleanup()
print("HFSS session closed and temporary files cleaned up")
Q3D Extractor Capacitance Extraction#
Now we simulate the same geometry using Q3D Extractor, which solves quasi-static electric fields to directly compute the capacitance matrix.
Comparison of approaches:
HFSS Driven Modal: Full-wave solve → S-parameters → Y-parameters → \(C_{12}\)
Q3D Extractor: Quasi-static solve → direct capacitance matrix
Analytical: Conformal mapping model (no simulation)
Q3D is particularly well suited for parasitic capacitance extraction because it directly solves the electrostatic (or quasi-static) problem, which is faster and more accurate at low frequencies than extracting capacitance from full-wave S-parameters.
References:
Q3D Extractor: https://aedt.docs.pyansys.com/version/stable/API/_autosummary/ansys.aedt.core.q3d.Q3d.html
Initialize Q3D Project#
Set up a Q3D Extractor project for capacitance extraction.
Note
This code requires an Ansys AEDT license (same as HFSS above).
from ansys.aedt.core import Q3d # noqa: E402
# Create temporary directory for Q3D project
temp_dir_q3d = tempfile.TemporaryDirectory(suffix=".ansys_qpdk_q3d")
project_path_q3d = Path(temp_dir_q3d.name) / "idc_q3d.aedt"
# Create temporary directory for Q3D project
temp_dir_q3d = tempfile.TemporaryDirectory(suffix=".ansys_qpdk_q3d")
project_path_q3d = Path(temp_dir_q3d.name) / "idc_q3d.aedt"
# Initialize Q3D Extractor
q3d = Q3d(
project=str(project_path_q3d),
design="InterdigitalCapacitor_Q3D",
non_graphical=False,
new_desktop=True,
version="2025.2",
)
q3d.modeler.model_units = "um"
# Initialize Q3D wrapper
q3d_sim = Q3D(q3d)
print(f"Q3D project created: {q3d.project_file}")
print(f"Design name: {q3d.design_name}")
print(f"Solution type: {q3d.solution_type}")
Import Geometry and Assign Signal Nets#
Import the same prepared component into Q3D and assign signal nets based on port locations. Each port becomes a separate conductor in the capacitance matrix.
The q3d_sim.assign_nets_from_ports method is the Q3D equivalent
of hfss_sim.add_lumped_ports — it maps gdsfactory port locations to
Q3D conductor nets.
# Import the prepared component geometry into Q3D
conductor_objects = q3d_sim.import_component(prepared_component)
print(f"Imported {len(conductor_objects)} conductor objects: {conductor_objects}")
# Add substrate below the component (Q3D modeler API is compatible with HFSS)
substrate_q3d_name = q3d_sim.add_substrate(
prepared_component,
thickness=500.0,
material="silicon",
)
print(f"Created substrate: {substrate_q3d_name}")
# Assign signal nets from port locations
signal_nets = q3d_sim.assign_nets_from_ports(
prepared_component.ports, conductor_objects
)
print(f"Assigned signal nets: {signal_nets}")
Configure and Run Q3D Analysis#
Set up a Q3D adaptive analysis at the same frequency used for HFSS. Q3D solves the quasi-static field problem and computes the full capacitance matrix between all signal nets.
# Create Q3D setup
q3d_setup = q3d.create_setup(name="Q3DSetup")
q3d_setup.props["AdaptiveFreq"] = f"{HFSS_CONFIG['solution_frequency_ghz']}GHz"
q3d_setup.props["Cap"]["MaxPass"] = 17
q3d_setup.props["Cap"]["MinPass"] = 2
q3d_setup.props["Cap"]["PerError"] = 0.5
# Disable AC and DC solving to avoid source/sink errors
q3d_setup.ac_rl_enabled = False
q3d_setup.dc_enabled = False
q3d_setup.capacitance_enabled = True
q3d_setup.update()
print("Q3D setup configured:")
print(f" - Adaptive frequency: {HFSS_CONFIG['solution_frequency_ghz']} GHz")
print("Starting Q3D analysis...")
print("(This is typically faster than full-wave HFSS)")
q3d.save_project()
start_time_q3d = time.time()
success_q3d = q3d.analyze_setup("Q3DSetup", cores=4)
elapsed_q3d = time.time() - start_time_q3d
if not success_q3d:
print("\nERROR: Q3D simulation failed!")
m = q3d.desktop_class.odesktop.GetMessages(q3d.project_name, q3d.design_name, 0)
for msg in m:
print(f"Desktop Msg: {msg}")
else:
print(f"Q3D analysis completed in {elapsed_q3d:.1f} seconds")
Extract Q3D Capacitance Matrix#
Q3D directly outputs the capacitance matrix between all signal nets. The off-diagonal element \(C_{12}\) gives the mutual capacitance, which corresponds to the coupling capacitance of the interdigital capacitor.
# Extract the capacitance matrix using the wrapper
cap_df = q3d_sim.get_capacitance_matrix(setup_name="Q3DSetup")
print("Q3D Capacitance Matrix (F):")
print(cap_df)
# Extract mutual capacitance |C12| from the off-diagonal element
C_q3d = None
for col in cap_df.columns:
# Match off-diagonal entries exactly between o1 and o2
if col in ["C(o1,o2)", "C(o2,o1)"]:
C_q3d = abs(float(cap_df[col][0]))
break
if C_q3d is not None:
print(f"\nQ3D mutual capacitance |C₁₂|: {C_q3d * 1e15:.2f} fF")
Q3D Cleanup#
q3d.save_project()
# q3d.release_desktop()
time.sleep(2)
temp_dir_q3d.cleanup()
print("Q3D session closed and temporary files cleaned up")
Comparison: Analytical vs HFSS vs Q3D#
Compare the capacitance values obtained from all three methods. The analytical model provides a quick estimate, the HFSS driven modal simulation gives the full-wave result, and Q3D Extractor provides a direct quasi-static capacitance extraction.
print("\n" + "=" * 58)
print(" Capacitance Comparison Summary")
print("=" * 58)
print(f"{'Method':<28} {'C (fF)':>10} {'Δ vs Analytical':>16}")
print("-" * 58)
C_analytical_fF = float(C_estimate) * 1e15
print(f"{'Analytical':<28} {C_analytical_fF:>10.2f} {'(reference)':>16}")
for freq_ghz, c_val in C_hfss_values.items():
c_fF = c_val * 1e15
delta = (c_val - float(C_estimate)) / float(C_estimate) * 100
label = f"HFSS @ {freq_ghz} GHz"
print(f"{label:<28} {c_fF:>10.2f} {delta:>+15.1f}%")
if C_q3d is not None:
C_q3d_fF = C_q3d * 1e15
delta_q3d = (C_q3d - float(C_estimate)) / float(C_estimate) * 100
print(f"{'Q3D Extractor':<28} {C_q3d_fF:>10.2f} {delta_q3d:>+15.1f}%")
print("=" * 58)
Summary#
This notebook demonstrated three approaches for characterizing an interdigital capacitor:
Analytical Estimate: Using the conformal mapping model for interdigital capacitors to get a quick capacitance estimate without simulation
HFSS Driven Modal Simulation:
Created the component geometry with QPDK and imported it into HFSS
Added lumped ports at CPW feed locations for S-parameter measurements
Ran a frequency sweep and extracted capacitance from Y-parameters (\(C_{12} = -\text{Im}(Y_{12}) / \omega\))
Q3D Extractor Simulation:
Imported the same geometry into Q3D Extractor
Assigned signal nets from gdsfactory port locations
Directly computed the capacitance matrix via quasi-static field solution
Key Takeaways:
Q3D Extractor directly solves for capacitance, making it ideal for parasitic extraction at low frequencies
HFSS driven modal captures frequency-dependent effects (parasitic inductance, radiation) that Q3D’s quasi-static approach does not
The analytical model provides a useful sanity check for both simulations
All three methods should agree well at low frequencies where the structure is electrically small
References#
Rui Igreja and C. J. Dias. Analytical evaluation of the interdigital electrodes capacitance for a multi-layered structure. Sensors and Actuators A: Physical, 112(2):291–301, May 2004. doi:10.1016/j.sna.2004.01.040.
David M. Pozar. Microwave Engineering. John Wiley & Sons, Inc., 4 edition, 2012. ISBN 978-0-470-63155-3.
Lei Zhu and Ke Wu. Accurate circuit model of interdigital capacitor and its application to design of new quasi-lumped miniaturized filters with suppression of harmonic resonance. IEEE Transactions on Microwave Theory and Techniques, 48(3):347–356, March 2000. doi:10.1109/22.826833.