Skip to content

Ansys HFSS, Q3D and 2D Extractor with PyAEDT

gplugins.ansys wraps PyAEDT applications so a gdsfactory component can be imported into Ansys Electronics Desktop with its layer stack and materials. You open the AEDT session yourself and hand the application to a wrapper:

  • gplugins.ansys.HFSS imports a component as 3D objects or PEC sheets, adds lumped ports and an air region, and reads eigenmode or S-parameter results.
  • gplugins.ansys.Q3D imports a component, assigns one signal net per port, and reads the capacitance matrix.
  • gplugins.ansys.Q2D draws a coplanar waveguide cross section for impedance extraction.

The layer-stack mapping and port geometry run anywhere. Anything that talks to AEDT needs a local installation and licence, so those cells are behind RUN_AEDT and are off by default.

Fully solved examples live in the Quantum RF PDK documentation: HFSS driven capacitor, HFSS eigenmode resonator and Q2D CPW impedance.

import gdsfactory as gf
import matplotlib.pyplot as plt
from gdsfactory.gpdk import PDK
from gdsfactory.technology import LayerLevel, LayerStack
from matplotlib.patches import Polygon, Rectangle

from gplugins.ansys import (
    layer_stack_to_gds_mapping,
    lumped_port_rectangle_from_cpw,
    object_names_to_materials,
)

PDK.activate()

#: Set to True on a machine with Ansys Electronics Desktop and a licence.
RUN_AEDT = False
#: AEDT release to start, e.g. "2025.2".
AEDT_VERSION = "2025.2"

METAL = (1, 0)
SUBSTRATE_LAYER = (99, 0)

Layer stack and materials

AEDT's GDS importer places each GDS layer number at one elevation and thickness, so the layer stack is turned into a {layer_number: (elevation, thickness)} map. Vacuum levels are skipped, and levels that share a layer number are merged when their spans join, or rejected when they cannot be told apart.

Materials come from a material_properties table. A material with infinite relative permittivity is a perfect conductor and becomes pec in AEDT; anything else keeps its name and permittivity. gplugins.ansys.DEFAULT_MATERIAL_PROPERTIES covers common names; pass your own table to the wrappers to change it.

stack = LayerStack(
    layers={
        "substrate": LayerLevel(
            layer=SUBSTRATE_LAYER, zmin=-500.0, thickness=500.0, material="si"
        ),
        "metal": LayerLevel(layer=METAL, zmin=0.0, thickness=0.2, material="Nb"),
    }
)
print(layer_stack_to_gds_mapping(stack))
print(object_names_to_materials(["metal", "substrate"], stack))
{99: (-500.0, 500.0), 1: (0.0, 0.2)}
{'metal': 'pec', 'substrate': 'si'}

A coplanar waveguide

A 10 µm centre conductor in a 6 µm gap, cut into a ground plane that also closes around both ends, with a port at each end of the centre conductor. lumped_port_rectangle_from_cpw places each lumped port in the end gap, between the port and the ground, with an integration line from the ground to the signal.

LENGTH = 300.0
CPW_WIDTH = 10.0
CPW_GAP = 6.0
GROUND_WIDTH = 100.0


def _box(x0: float, y0: float, x1: float, y1: float) -> gf.kdb.DBox:
    return gf.kdb.DBox(x0, y0, x1, y1)


@gf.cell
def cpw_with_ground() -> gf.Component:
    c = gf.Component()
    inner = CPW_WIDTH / 2 + CPW_GAP
    outer = inner + GROUND_WIDTH
    ground = gf.kdb.Region(_box(-GROUND_WIDTH, -outer, LENGTH + GROUND_WIDTH, outer).to_itype(c.kcl.dbu))
    trench = gf.kdb.Region(_box(-CPW_GAP, -inner, LENGTH + CPW_GAP, inner).to_itype(c.kcl.dbu))
    signal = gf.kdb.Region(_box(0, -CPW_WIDTH / 2, LENGTH, CPW_WIDTH / 2).to_itype(c.kcl.dbu))
    c.add_polygon(ground - trench + signal, layer=METAL)
    c.add_port("o1", center=(0, 0), width=CPW_WIDTH, orientation=180, layer=METAL)
    c.add_port("o2", center=(LENGTH, 0), width=CPW_WIDTH, orientation=0, layer=METAL)
    return c


component = cpw_with_ground()

fig, ax = plt.subplots(figsize=(8, 4))
for points in component.get_polygons_points(layers=[METAL], by="tuple")[METAL]:
    ax.add_patch(Polygon(points, color="tab:grey"))
for port in component.ports:
    rect = lumped_port_rectangle_from_cpw(
        (*port.center, 0.0), port.orientation, CPW_GAP, CPW_WIDTH
    )
    (x, y, _), (dx, dy) = rect["origin"], rect["sizes"]
    ax.add_patch(Rectangle((x, y), dx, dy, color="tab:red", alpha=0.6))
    (x0, y0, _), (x1, y1, _) = rect["integration_line"]
    ax.annotate("", (x1, y1), (x0, y0), arrowprops={"arrowstyle": "->"})
    print(port.name, rect)
ax.set_xlim(-20, LENGTH + 20)
ax.set_ylim(-40, 40)
ax.set_aspect("equal")
o1 {'origin': [np.float64(-6.0), np.float64(-5.0), 0.0], 'sizes': [np.float64(6.0), np.float64(10.0)], 'integration_line': [[np.float64(-6.0), np.float64(0.0), 0.0], [0.0, 0.0, 0.0]]}
o2 {'origin': [np.float64(300.0), np.float64(-5.0), 0.0], 'sizes': [np.float64(6.0), np.float64(10.0)], 'integration_line': [[np.float64(306.0), np.float64(0.0), 0.0], [300.0, 0.0, 0.0]]}

png

HFSS driven modal

The metal is imported as PEC sheets on a silicon substrate, inside a vacuum box with a radiation boundary. Solver settings are plain PyAEDT.

if RUN_AEDT:
    from ansys.aedt.core import Hfss

    from gplugins.ansys import HFSS, detach_desktop_logging

    hfss = Hfss(
        design="CPW",
        solution_type="DrivenModal",
        non_graphical=True,
        new_desktop=True,
        version=AEDT_VERSION,
    )
    # PyAEDT reads the desktop on every log message, which can drop the session.
    detach_desktop_logging(hfss)
    sim = HFSS(hfss, layer_stack=stack)
    sim.import_component(component, import_as_sheets=True)
    sim.add_substrate(component, thickness=500.0, material="silicon")
    air = sim.add_air_region(component, height=500.0, substrate_thickness=500.0)
    hfss.assign_radiation_boundary_to_objects(air)
    sim.add_lumped_ports(component.ports, CPW_GAP, CPW_WIDTH)

    setup = hfss.create_setup(name="DrivenSetup", Frequency="6GHz")
    setup.create_frequency_sweep(
        unit="GHz",
        name="FrequencySweep",
        start_frequency=1.0,
        stop_frequency=10.0,
        sweep_type="Interpolating",
        num_of_freq_points=201,
    )
    hfss.analyze_setup("DrivenSetup")
    print(sim.get_sparameter_results("DrivenSetup", "FrequencySweep"))
    hfss.release_desktop()
else:
    print("RUN_AEDT is False; skipping HFSS.")
RUN_AEDT is False; skipping HFSS.

Q3D capacitance

Q3D needs one net per conductor. assign_nets_from_ports names the conductor nearest each port after the port; the rest stay as auto-identified nets. Here both ports sit on the one centre conductor, so it becomes net o1 and o2 is skipped with a warning.

if RUN_AEDT:
    from ansys.aedt.core import Q3d

    from gplugins.ansys import Q3D, detach_desktop_logging

    q3d = Q3d(design="CPW_Q3D", non_graphical=True, new_desktop=True, version=AEDT_VERSION)
    detach_desktop_logging(q3d)
    q3d_sim = Q3D(q3d, layer_stack=stack)
    conductors = q3d_sim.import_component(component)
    q3d_sim.add_substrate(component, thickness=500.0, material="silicon")
    q3d_sim.assign_nets_from_ports(component.ports, conductors)

    q3d_setup = q3d.create_setup(name="Q3DSetup")
    q3d_setup.ac_rl_enabled = False
    q3d_setup.dc_enabled = False
    q3d_setup.capacitance_enabled = True
    q3d_setup.update()
    q3d.analyze_setup("Q3DSetup")
    print(q3d_sim.get_capacitance_matrix("Q3DSetup"))
    q3d.release_desktop()
else:
    print("RUN_AEDT is False; skipping Q3D.")
RUN_AEDT is False; skipping Q3D.

2D Extractor impedance

create_2d_cpw reads the substrate and conductor thickness and material from the named layer stack levels. Conductors thinner than 2 µm are thickened for solver stability.

if RUN_AEDT:
    from ansys.aedt.core import Q2d

    from gplugins.ansys import Q2D, detach_desktop_logging

    q2d = Q2d(design="CPW_Q2D", non_graphical=True, new_desktop=True, version=AEDT_VERSION)
    detach_desktop_logging(q2d)
    Q2D(q2d, layer_stack=stack).create_2d_cpw(
        CPW_WIDTH, CPW_GAP, substrate_level="substrate", conductor_level="metal"
    )
    q2d_setup = q2d.create_setup(name="Q2DSetup")
    q2d.analyze_setup("Q2DSetup")
    data = q2d.post.get_solution_data(
        expressions="Z0(signal,signal)", setup_sweep_name="Q2DSetup : LastAdaptive"
    )
    _, z0 = data.get_expression_data(formula="real")
    print(f"Z0 = {z0[0]:.2f} Ohm")
    q2d.release_desktop()
else:
    print("RUN_AEDT is False; skipping Q2D.")
RUN_AEDT is False; skipping Q2D.