Palace is an open-source 3D electromagnetic simulator supporting eigenmode, driven (S-parameter), and electrostatic simulations. This notebook demonstrates using the gsim.palace API to run a driven simulation on a CPW (coplanar waveguide) structure.

Requirements:

  • IHP PDK: uv pip install ihp-gdsfactory
  • GDSFactory+ account for cloud simulation

Load a pcell from IHP PDK

import gdsfactory as gf
from ihp import LAYER, PDK

PDK.activate()

# IHP SG13G2 via design rules
_VIA_RULES = {
    "TopVia2": {
        "size": 0.9,
        "spacing": 1.06,
        "enclosure": 0.5,
        "layer": LAYER.TopVia2drawing,
    },
    "TopVia1": {
        "size": 0.42,
        "spacing": 0.42,
        "enclosure": 0.42,
        "layer": LAYER.TopVia1drawing,
    },
}


@gf.cell
def _via_block(
    cols: int = 2,
    rows: int = 2,
    via_type: str = "TopVia2",
) -> gf.Component:
    """Create a block of vias following IHP design rules."""
    c = gf.Component()
    rules = _VIA_RULES[via_type]
    size = rules["size"]
    pitch = size + rules["spacing"]  # center-to-center

    via = gf.c.rectangle((size, size), layer=rules["layer"])
    for col in range(cols):
        for row in range(rows):
            ref = c << via
            ref.move((col * pitch, row * pitch))

    return c


def _via_pad_width(cols: int, via_type: str) -> float:
    """Width of via pad (via array + enclosure on both sides)."""
    rules = _VIA_RULES[via_type]
    pitch = rules["size"] + rules["spacing"]
    return (cols - 1) * pitch + rules["size"] + 2 * rules["enclosure"]


def _place_via_block(c, via_block, x, y_ctr, via_type, cols, rows):
    """Place a via block centered vertically at y_ctr."""
    rules = _VIA_RULES[via_type]
    pitch = rules["size"] + rules["spacing"]
    vb = c << via_block
    vb.move((x + rules["enclosure"], y_ctr - ((rows - 1) * pitch + rules["size"]) / 2))


@gf.cell
def gsg_electrode_tm2_tm1_m5(
    tm2_length: float = 100,
    tm1_length: float = 50,
    m5_length: float = 300,
    s_width: float = 20,
    g_width: float = 40,
    gap_width: float = 15,
    tv2_cols: int = 2,
    tv2_rows: int = 10,
    tv1_cols: int = 4,
    tv1_rows: int = 10,
) -> gf.Component:
    """GSG electrode: TM2 -> TopVia2 -> TM1 -> TopVia1 -> M5 -> TopVia1 -> TM1 -> TopVia2 -> TM2.

    Args:
        tm2_length: Length of TM2 sections at each end (um)
        tm1_length: Length of TM1 sections between vias (um)
        m5_length: Length of Metal5 section in the middle (um)
        s_width: Signal trace width (um)
        g_width: Ground trace width (um)
        gap_width: Gap between signal and ground (um)
        tv2_cols/rows: TopVia2 array size
        tv1_cols/rows: TopVia1 array size
    """
    c = gf.Component()

    tv2_w = _via_pad_width(tv2_cols, "TopVia2")
    tv1_w = _via_pad_width(tv1_cols, "TopVia1")

    # Total length: TM2 | TV2 | TM1 | TV1 | M5 | TV1 | TM1 | TV2 | TM2
    total = 2 * tm2_length + 2 * tv2_w + 2 * tm1_length + 2 * tv1_w + m5_length

    # Section x-coordinates (left edges, centered at x=0)
    x = -total / 2
    sections = [
        ("tm2_l", tm2_length),
        ("tv2_l", tv2_w),
        ("tm1_l", tm1_length),
        ("tv1_l", tv1_w),
        ("m5", m5_length),
        ("tv1_r", tv1_w),
        ("tm1_r", tm1_length),
        ("tv2_r", tv2_w),
        ("tm2_r", tm2_length),
    ]
    xs = {}
    for name, width in sections:
        xs[name] = x
        x += width

    # Via blocks
    tv2_block = _via_block(cols=tv2_cols, rows=tv2_rows, via_type="TopVia2")
    tv1_block = _via_block(cols=tv1_cols, rows=tv1_rows, via_type="TopVia1")

    # GSG traces
    traces = [
        (0, s_width),
        (s_width / 2 + gap_width + g_width / 2, g_width),
        (-(s_width / 2 + gap_width + g_width / 2), g_width),
    ]

    TM2 = LAYER.TopMetal2drawing
    TM1 = LAYER.TopMetal1drawing
    M5 = LAYER.Metal5drawing

    for y_ctr, w in traces:
        yb = y_ctr - w / 2

        # TM2 left
        (c << gf.c.rectangle((tm2_length, w), layer=TM2)).move((xs["tm2_l"], yb))

        # TopVia2 left transition (TM2 + TM1 overlap + vias)
        (c << gf.c.rectangle((tv2_w, w), layer=TM2)).move((xs["tv2_l"], yb))
        (c << gf.c.rectangle((tv2_w, w), layer=TM1)).move((xs["tv2_l"], yb))
        _place_via_block(
            c, tv2_block, xs["tv2_l"], y_ctr, "TopVia2", tv2_cols, tv2_rows
        )

        # TM1 left
        (c << gf.c.rectangle((tm1_length, w), layer=TM1)).move((xs["tm1_l"], yb))

        # TopVia1 left transition (TM1 + M5 overlap + vias)
        (c << gf.c.rectangle((tv1_w, w), layer=TM1)).move((xs["tv1_l"], yb))
        (c << gf.c.rectangle((tv1_w, w), layer=M5)).move((xs["tv1_l"], yb))
        _place_via_block(
            c, tv1_block, xs["tv1_l"], y_ctr, "TopVia1", tv1_cols, tv1_rows
        )

        # Metal5 middle
        (c << gf.c.rectangle((m5_length, w), layer=M5)).move((xs["m5"], yb))

        # TopVia1 right transition
        (c << gf.c.rectangle((tv1_w, w), layer=TM1)).move((xs["tv1_r"], yb))
        (c << gf.c.rectangle((tv1_w, w), layer=M5)).move((xs["tv1_r"], yb))
        _place_via_block(
            c, tv1_block, xs["tv1_r"], y_ctr, "TopVia1", tv1_cols, tv1_rows
        )

        # TM1 right
        (c << gf.c.rectangle((tm1_length, w), layer=TM1)).move((xs["tm1_r"], yb))

        # TopVia2 right transition
        (c << gf.c.rectangle((tv2_w, w), layer=TM2)).move((xs["tv2_r"], yb))
        (c << gf.c.rectangle((tv2_w, w), layer=TM1)).move((xs["tv2_r"], yb))
        _place_via_block(
            c, tv2_block, xs["tv2_r"], y_ctr, "TopVia2", tv2_cols, tv2_rows
        )

        # TM2 right
        (c << gf.c.rectangle((tm2_length, w), layer=TM2)).move((xs["tm2_r"], yb))

    # Ports at TM2 ends
    c.add_port(
        name="o1",
        center=(-total / 2, 0),
        width=s_width,
        orientation=0,
        port_type="electrical",
        layer=TM2,
    )
    c.add_port(
        name="o2",
        center=(total / 2, 0),
        width=s_width,
        orientation=180,
        port_type="electrical",
        layer=TM2,
    )

    return c


c = gsg_electrode_tm2_tm1_m5()
cc = c.copy()
cc.draw_ports()
cc

png

Configure and run simulation with DrivenSim

from gsim.palace import DrivenSim

# Create simulation object
sim = DrivenSim()

# Set output directory
sim.set_output_dir("./palace-sim-cpw")

# Set the component geometry
sim.set_geometry(c)

# Configure layer stack from active PDK
sim.set_stack(substrate_thickness=2.0, air_above=300.0)

# Configure left CPW port (single port at signal center)
sim.add_cpw_port("o1", layer="topmetal2", s_width=20, gap_width=15)

# Configure right CPW port (single port at signal center)
sim.add_cpw_port("o2", layer="topmetal2", s_width=20, gap_width=15)

# Configure driven simulation (frequency sweep for S-parameters)
sim.set_driven(fmin=1e9, fmax=100e9, num_points=300)

# Validate configuration
print(sim.validate_config())
Validation: PASSED
# Generate mesh with planar conductors (presets: "coarse", "default", "fine")
sim.mesh(preset="default", planar_conductors=False)
Warning : 1 ill-shaped tets are still in the mesh


Warning : ------------------------------
Warning : Mesh generation error summary
Warning :     1 warning
Warning :     0 errors
Warning : Check the full log for details
Warning : ------------------------------





Mesh Summary
========================================
Dimensions: 715.3 x 230.0 x 318.3 µm
Nodes:      7,925
Elements:   61,552
Tetrahedra: 43,920
Edge length: 0.05 - 246.53 µm
Quality:    0.453 (min: 0.001)
SICN:       0.496 (all valid)
----------------------------------------
Volumes (5):
  - topvia1 [1]
  - topvia2 [2]
  - SiO2 [3]
  - passive [4]
  - air [5]
Surfaces (17):
  - metal5_xy [6]
  - metal5_z [7]
  - topmetal1_xy [8]
  - topmetal1_z [9]
  - topmetal2_xy [10]
  - topmetal2_z [11]
  - P1_E0 [12]
  - P1_E1 [13]
  - P2_E0 [14]
  - P2_E1 [15]
  - SiO2__topvia1 [16]
  - SiO2__topvia2 [17]
  - SiO2__None [18]
  - SiO2__passive [19]
  - passive__None [20]
  - air__passive [21]
  - air__None [22]
----------------------------------------
Mesh:   palace-sim-cpw/palace.msh
sim.plot_mesh(show_groups=["metal", "P", "via"])
/home/runner/work/gsim/gsim/src/gsim/viz.py:261: UserWarning: Failed to use notebook backend:

Please install `nest_asyncio2` to automagically launch the trame server without await. Or, to avoid `nest_asyncio2` run:

from pyvista.trame.jupyter import launch_server
await launch_server().ready

Falling back to a static output.
  plotter.show()

png

Run simulation on cloud

# Run simulation on GDSFactory+ cloud
results = sim.run()
  palace-db938763  completed  7m 58s


Extracting results.tar.gz...
Downloaded 10 files to /home/runner/work/gsim/gsim/nbs/sim-data-palace-db938763
from gsim.palace import load_sparams

sp = load_sparams(results)
sp.plot()

png