"""Volumetric-FEM helpers for qpdk layouts.
Converts the PDK's subtractive etch masks into the explicit conductor and
dielectric regions that full-wave 3-D solvers (Palace through gsim, Meep, ...)
expect, and provides the matching layer stacks. Install gsim with the ``palace``
dependency group on Python 3.12. Functions that need it import it lazily.
"""
from __future__ import annotations
from collections.abc import Collection
from typing import Any, Literal
import gdsfactory as gf
import klayout.db as kdb
from kfactory.layer import LayerEnum
from qpdk.tech import CHIP_LAYERS, LAYER, NON_METADATA_LAYERS, Chip, material_properties
__all__ = [
"FEM_LAYERS",
"FLIP_CHIP_FEM_LAYERS",
"TSV_FEM_LAYERS",
"flip_chip_stack",
"single_chip_stack",
"to_fem_regions",
"to_flip_chip_regions",
"to_tsv_regions",
"tsv_stack",
]
# Layer numbering understood by gsim's Palace and Meep workflows: dielectric
# substrate, zero-thickness conductor sheet, and the vacuum above the chip.
# The GDS numbers come from the unused simulation-only block, next to
# SIM_AREA (98,0) / SIM_ONLY (99,0): they must not collide with real mask
# layers (M1_DRAW is (1,0), M2_DRAW (2,0), ...), or the PDK's own
# additive-metal pass would destroy a written-back FEM component and any
# GDS round-trip would show dielectric as metal.
FEM_LAYERS = {"SUBSTRATE": (91, 0), "SUPERCONDUCTOR": (92, 0), "VACUUM": (93, 0)}
def _reject_unsupported_layers(
component: gf.Component, supported: Collection[LayerEnum]
) -> None:
"""Reject fabrication masks that would be silently lost by conversion.
Raises:
ValueError: If an unsupported fabrication layer carries polygons.
"""
unsupported = {
tuple(layer): str(layer)
for layer in NON_METADATA_LAYERS
if layer not in supported
}
layout = component.kdb_cell.layout()
present = []
for index in layout.layer_indices():
info = layout.get_info(index)
spec = (info.layer, info.datatype)
if (
spec in unsupported
and not kdb.Region(component.kdb_cell.begin_shapes_rec(index)).is_empty()
):
present.append(unsupported[spec])
if present:
raise ValueError(
f"FEM conversion does not model fabrication layers {sorted(present)}"
)
[docs]
def to_fem_regions(component: gf.Component) -> gf.Component:
"""Convert an etch-based qpdk layout into explicit FEM regions.
The PDK draws metal subtractively: ``M1_ETCH`` marks where metal is
removed and additive shapes on ``M1_DRAW`` punch through the etch mask
(the conductor is ``SIM_AREA - (M1_ETCH - M1_DRAW)``), matching
``get_layer_stack()``'s M1 definition. The conductor, substrate and
vacuum regions are copied onto the layer numbering of
:data:`FEM_LAYERS`, with the zero-thickness conductor later becoming a
perfect-electric-conductor boundary in the solver. Junction masks are
omitted because lumped ports represent them. Other fabrication layers
outside M1 are rejected.
Args:
component: Layout with a ``SIM_AREA`` layer and simulation ports.
Returns:
Component carrying the ``SUBSTRATE``, ``SUPERCONDUCTOR`` and
``VACUUM`` regions plus the input ports.
Raises:
ValueError: If unsupported fabrication layers are present, or the
component carries no ``SIM_AREA`` shapes or the
simulation area is fully etched away, either of which would
silently produce an empty FEM model.
"""
_reject_unsupported_layers(
component, {LAYER.M1_DRAW, LAYER.M1_ETCH, LAYER.JJ_AREA, LAYER.JJ_PATCH}
)
layout = component.kdb_cell.layout()
sim_region = kdb.Region(
component.kdb_cell.begin_shapes_rec(layout.layer(*LAYER.SIM_AREA))
)
etch_region = kdb.Region(
component.kdb_cell.begin_shapes_rec(layout.layer(*LAYER.M1_ETCH))
)
draw_region = kdb.Region(
component.kdb_cell.begin_shapes_rec(layout.layer(*LAYER.M1_DRAW))
)
conductor_region = sim_region - (etch_region - draw_region)
if sim_region.is_empty():
msg = "no SIM_AREA layer in the component; the FEM model would be empty"
raise ValueError(msg)
if conductor_region.is_empty():
msg = "SIM_AREA is fully etched away; the FEM conductor would be empty"
raise ValueError(msg)
etched = gf.Component(kcl=component.kcl)
el = etched.kdb_cell.layout()
for name, region in [
("SUPERCONDUCTOR", conductor_region),
("SUBSTRATE", sim_region),
("VACUUM", sim_region),
]:
etched.kdb_cell.shapes(el.layer(*FEM_LAYERS[name])).insert(region)
for port in component.ports:
etched.add_port(name=port.name, port=port)
return etched
[docs]
def single_chip_stack(
substrate_thickness: float = 500.0, vacuum_thickness: float = 500.0
) -> Any:
r"""Return a gsim layer stack for a single qpdk silicon chip.
The substrate uses the microwave silicon from the qpdk material
properties (:math:`\varepsilon_r = 11.45`,
:math:`\tan\delta = 2.7 \times 10^{-6}`), so FEM and analytical CPW
models describe the same chip. The zero-thickness niobium conductor
becomes a perfect electric conductor in the solver; its material entry
only names the layer.
Args:
substrate_thickness: Silicon thickness in μm.
vacuum_thickness: Air height above the chip in μm.
Returns:
A ``gsim.common.stack.LayerStack`` (requires the ``palace``
dependency group on Python 3.12).
"""
from gsim.common.stack import Layer, LayerStack
from gsim.common.stack.materials import MATERIALS_DB
silicon = material_properties["Si"]
stack = LayerStack(pdk_name="qpdk")
stack.layers["SUBSTRATE"] = Layer(
name="SUBSTRATE",
gds_layer=FEM_LAYERS["SUBSTRATE"],
zmin=0.0,
zmax=substrate_thickness,
thickness=substrate_thickness,
material="qpdk-silicon",
layer_type="dielectric",
)
stack.layers["SUPERCONDUCTOR"] = Layer(
name="SUPERCONDUCTOR",
gds_layer=FEM_LAYERS["SUPERCONDUCTOR"],
zmin=substrate_thickness,
zmax=substrate_thickness,
thickness=0,
material="qpdk-niobium",
layer_type="conductor",
)
stack.layers["VACUUM"] = Layer(
name="VACUUM",
gds_layer=FEM_LAYERS["VACUUM"],
zmin=substrate_thickness,
zmax=substrate_thickness + vacuum_thickness,
thickness=vacuum_thickness,
material="vacuum",
layer_type="dielectric",
)
stack.materials = {
"qpdk-silicon": {
"permittivity": silicon["relative_permittivity"],
"loss_tangent": silicon["loss_tangent"],
},
"qpdk-niobium": {"permittivity": 1.0},
"vacuum": MATERIALS_DB["vacuum"].to_dict(),
}
return stack
# Layer numbering for two-chip (flip-chip) models: the bottom chip sits below
# the inter-chip gap, the top chip is flipped so its metal faces down. Keep
# these outside the mask and label layer numbers.
FLIP_CHIP_FEM_LAYERS = {
"SUBSTRATE": (114, 0),
"M1": (115, 0),
"VACUUM": (116, 0),
"BUMP": (117, 0),
"M2": (118, 0),
"SUBSTRATE_TOP": (119, 0),
}
[docs]
def to_flip_chip_regions(component: gf.Component) -> gf.Component:
"""Convert a two-metal-layer qpdk layout into explicit flip-chip regions.
Both metal levels follow the same subtractive convention as
:func:`to_fem_regions` (conductor is ``SIM_AREA - (ETCH - DRAW)``, per
``LAYER_STACK_FLIP_CHIP``), and the indium bump layer is copied to its
own region. The regions land on the layer numbering of
:data:`FLIP_CHIP_FEM_LAYERS`: two substrate slabs, two zero-thickness
metal sheets, the vacuum gap, and the bumps. Junction masks are omitted
because lumped ports represent them. Other fabrication layers are rejected.
Args:
component: Flip-chip layout with a ``SIM_AREA`` layer, M1/M2 metal
and ``IND`` bump shapes, plus simulation ports.
Returns:
Component carrying the flip-chip regions plus the input ports.
Raises:
ValueError: If unsupported fabrication layers are present, the
component carries no ``SIM_AREA`` shapes, a metal
level has neither etch nor draw shapes (which the subtractive
convention would turn into a solid metal plane shorting the
model), is fully etched away, or there are no indium bumps to
connect the two chips.
"""
_reject_unsupported_layers(
component,
{
LAYER.M1_DRAW,
LAYER.M1_ETCH,
LAYER.M2_DRAW,
LAYER.M2_ETCH,
LAYER.IND,
LAYER.JJ_AREA,
LAYER.JJ_PATCH,
},
)
layout = component.kdb_cell.layout()
sim_region = kdb.Region(
component.kdb_cell.begin_shapes_rec(layout.layer(*LAYER.SIM_AREA))
)
if sim_region.is_empty():
msg = "no SIM_AREA layer in the component; the FEM model would be empty"
raise ValueError(msg)
def _level_conductor(
etch: tuple[int, int], draw: tuple[int, int], name: str
) -> kdb.Region:
"""Conductor = SIM_AREA - ETCH + DRAW for one metal level."""
etch_region = kdb.Region(
component.kdb_cell.begin_shapes_rec(layout.layer(*etch))
)
draw_region = kdb.Region(
component.kdb_cell.begin_shapes_rec(layout.layer(*draw))
)
if etch_region.is_empty() and draw_region.is_empty():
# A level with no etch and no draw shapes would degenerate to a
# solid metal plane, silently shorting the model.
msg = (
f"metal level {name} has neither etch nor draw shapes;"
" the subtractive convention would make it a solid plane"
)
raise ValueError(msg)
conductor = ((sim_region - etch_region) | (draw_region & sim_region)).merged()
if conductor.is_empty():
raise ValueError(f"metal level {name} is fully etched away")
return conductor
m1_region = _level_conductor(LAYER.M1_ETCH, LAYER.M1_DRAW, "M1")
m2_region = _level_conductor(LAYER.M2_ETCH, LAYER.M2_DRAW, "M2")
# Bumps outside the simulation area would poke into empty space and
# grow the model bounding box, so clip them to it.
bump_region = (
kdb.Region(component.kdb_cell.begin_shapes_rec(layout.layer(*LAYER.IND)))
& sim_region
).merged()
if bump_region.is_empty():
msg = "no indium bumps in the component; the two chips would not connect"
raise ValueError(msg)
regions = gf.Component(kcl=component.kcl)
rl = regions.kdb_cell.layout()
for name, region in [
("SUBSTRATE", sim_region),
("M1", m1_region),
("VACUUM", sim_region),
("BUMP", bump_region),
("M2", m2_region),
("SUBSTRATE_TOP", sim_region),
]:
regions.kdb_cell.shapes(rl.layer(*FLIP_CHIP_FEM_LAYERS[name])).insert(region)
for port in component.ports:
regions.add_port(name=port.name, port=port)
return regions
[docs]
def flip_chip_stack(
substrate_thickness: float = 500.0,
bump_thickness: float = 10.0,
) -> Any:
"""Return a gsim layer stack for a face-to-face qpdk flip-chip pair.
The bottom chip carries M1 with the substrate below it; the top chip is
flipped so its M2 faces down across the indium-bump gap, with its
substrate above. Both metals become perfect electric conductors and the
bumps conductive vias; the substrate uses the qpdk microwave silicon, so
FEM and analytical models describe the same chips.
Args:
substrate_thickness: Silicon thickness of each chip in μm.
bump_thickness: Gap height between the chips (bump height) in μm.
Returns:
A ``gsim.common.stack.LayerStack`` (requires the ``palace``
dependency group on Python 3.12).
"""
from gsim.common.stack import Layer, LayerStack
from gsim.common.stack.materials import MATERIALS_DB
silicon = material_properties["Si"]
stack = LayerStack(pdk_name="qpdk")
stack.layers["SUBSTRATE"] = Layer(
name="SUBSTRATE",
gds_layer=FLIP_CHIP_FEM_LAYERS["SUBSTRATE"],
zmin=-substrate_thickness,
zmax=0.0,
thickness=substrate_thickness,
material="qpdk-silicon",
layer_type="dielectric",
)
stack.layers["M1"] = Layer(
name="M1",
gds_layer=FLIP_CHIP_FEM_LAYERS["M1"],
zmin=0.0,
zmax=0.0,
thickness=0,
material="qpdk-niobium",
layer_type="conductor",
)
stack.layers["VACUUM"] = Layer(
name="VACUUM",
gds_layer=FLIP_CHIP_FEM_LAYERS["VACUUM"],
zmin=0.0,
zmax=bump_thickness,
thickness=bump_thickness,
material="vacuum",
layer_type="dielectric",
)
stack.layers["BUMP"] = Layer(
name="BUMP",
gds_layer=FLIP_CHIP_FEM_LAYERS["BUMP"],
zmin=0.0,
zmax=bump_thickness,
thickness=bump_thickness,
material="qpdk-indium",
layer_type="via",
)
stack.layers["M2"] = Layer(
name="M2",
gds_layer=FLIP_CHIP_FEM_LAYERS["M2"],
zmin=bump_thickness,
zmax=bump_thickness,
thickness=0,
material="qpdk-niobium",
layer_type="conductor",
)
stack.layers["SUBSTRATE_TOP"] = Layer(
name="SUBSTRATE_TOP",
gds_layer=FLIP_CHIP_FEM_LAYERS["SUBSTRATE_TOP"],
zmin=bump_thickness,
zmax=bump_thickness + substrate_thickness,
thickness=substrate_thickness,
material="qpdk-silicon",
layer_type="dielectric",
)
stack.materials = {
"qpdk-silicon": {
"permittivity": silicon["relative_permittivity"],
"loss_tangent": silicon["loss_tangent"],
},
"qpdk-niobium": {"permittivity": 1.0},
# Indium σ from literature; without a conductivity gsim degrades the
# via to a 2-D PEC sheet at its base, which no longer bridges the
# two conductor planes.
"qpdk-indium": {"conductivity": 1.16e7},
"vacuum": MATERIALS_DB["vacuum"].to_dict(),
}
return stack
# Layer numbering for single-chip models with metal on both faces joined by
# through-silicon vias: M1 on the front face, MB on the back face, and the
# vias through the substrate between them. The numbers continue the
# simulation-only block after FLIP_CHIP_FEM_LAYERS. For the flip-chip top chip
# (``to_tsv_regions(..., chip="top")``) M1 holds its front metal M2 and MB its
# backside metal MB2.
TSV_FEM_LAYERS = {
"VACUUM_BOTTOM": (108, 0), # air below the chip
"MB": (109, 0), # backside metal (zero-thickness PEC)
"SUBSTRATE": (110, 0), # silicon
"TSV": (111, 0), # through-silicon vias (conductive)
"M1": (112, 0), # front-side metal (zero-thickness PEC)
"VACUUM": (113, 0), # air above the chip
}
def _conductor_region(
component: gf.Component,
sim_region: kdb.Region,
etch: tuple[int, int],
draw: tuple[int, int],
name: str,
) -> kdb.Region:
"""Return ``SIM_AREA - ETCH + DRAW`` for one metal level.
Raises:
ValueError: If the level has neither etch nor draw shapes, which the
subtractive convention would turn into a solid metal plane.
"""
layout = component.kdb_cell.layout()
etch_region = kdb.Region(component.kdb_cell.begin_shapes_rec(layout.layer(*etch)))
draw_region = kdb.Region(component.kdb_cell.begin_shapes_rec(layout.layer(*draw)))
if etch_region.is_empty() and draw_region.is_empty():
msg = (
f"metal level {name} has neither etch nor draw shapes;"
" the subtractive convention would make it a solid plane"
)
raise ValueError(msg)
return ((sim_region - etch_region) | (draw_region & sim_region)).merged()
def _metal_name(draw: tuple[int, int]) -> str:
"""Metal level name of a ``*_DRAW`` layer, e.g. ``MB`` for ``MB_DRAW``."""
return str(draw).removesuffix("_DRAW")
[docs]
def to_tsv_regions(component: gf.Component, chip: Chip = "bottom") -> gf.Component:
"""Convert a double-sided qpdk layout into explicit TSV FEM regions.
The front (M1) and back (MB) metal levels both follow the subtractive
convention of :func:`to_fem_regions` (conductor is
``SIM_AREA - (ETCH - DRAW)``, per ``LAYER_STACK_BACKSIDE``), and the
``TSV`` layer is copied to its own region. Backside shapes are drawn in
the front-side frame, as in :func:`~qpdk.cells.tsv.tsv_transition_double_sided`,
so no mirroring is applied. The regions land on the layer numbering of
:data:`TSV_FEM_LAYERS`. With ``chip="top"`` the flip-chip top chip is
converted instead, reading M2, MB2 and TSV2 into the same regions.
Args:
component: Layout with a ``SIM_AREA`` layer, front and back metal and
TSV shapes of ``chip``, plus simulation ports.
chip: Chip to convert, ``"bottom"`` or ``"top"``, see
:data:`~qpdk.tech.CHIP_LAYERS`.
Returns:
Component carrying the TSV regions plus the input ports.
Raises:
ValueError: If the component carries no ``SIM_AREA`` shapes, a metal
level has neither etch nor draw shapes, or there are no TSVs
inside the simulation area to connect the two faces.
"""
layout = component.kdb_cell.layout()
sim_region = kdb.Region(
component.kdb_cell.begin_shapes_rec(layout.layer(*LAYER.SIM_AREA))
)
if sim_region.is_empty():
msg = "no SIM_AREA layer in the component; the FEM model would be empty"
raise ValueError(msg)
layers = CHIP_LAYERS[chip]
m1_region = _conductor_region(
component,
sim_region,
layers.front_etch,
layers.front_draw,
_metal_name(layers.front_draw),
)
mb_region = _conductor_region(
component,
sim_region,
layers.back_etch,
layers.back_draw,
_metal_name(layers.back_draw),
)
tsv_region = (
kdb.Region(component.kdb_cell.begin_shapes_rec(layout.layer(*layers.tsv)))
& sim_region
).merged()
if tsv_region.is_empty():
msg = "no TSVs in the simulation area; the two faces would not connect"
raise ValueError(msg)
regions = gf.Component()
rl = regions.kdb_cell.layout()
for name, region in [
("VACUUM_BOTTOM", sim_region),
("MB", mb_region),
("SUBSTRATE", sim_region),
("TSV", tsv_region),
("M1", m1_region),
("VACUUM", sim_region),
]:
regions.kdb_cell.shapes(rl.layer(*TSV_FEM_LAYERS[name])).insert(region)
for port in component.ports:
regions.add_port(name=port.name, port=port)
return regions
[docs]
def tsv_stack(
substrate_thickness: float = 200.0,
vacuum_thickness: float = 500.0,
tsv_conductivity: float = 1.0e6,
) -> Any:
"""Return a gsim layer stack for a chip with metal on both faces.
The front metal M1 sits at ``z = 0`` on top of the silicon and the
backside metal MB at ``z = -substrate_thickness`` below it; both become
perfect electric conductors. The TSVs are conductive vias spanning the
full substrate. Air fills ``vacuum_thickness`` above and below the chip.
The default substrate thickness follows the 200 µm TSV depth of
:cite:`mallekFabricationSuperconductingThroughsilicon2021`.
Args:
substrate_thickness: Silicon thickness (TSV depth) in μm.
vacuum_thickness: Air height above and below the chip in μm.
tsv_conductivity: Normal-state conductivity of the via lining in
S/m. The lining is superconducting at operating temperature;
any finite value large enough to make the via a good conductor
works here. Without a conductivity gsim degrades the via to a
2-D PEC sheet at its base, which no longer bridges the faces.
Returns:
A ``gsim.common.stack.LayerStack`` (requires gsim from the ``models``
extra).
"""
from gsim.common.stack import Layer, LayerStack
from gsim.common.stack.materials import MATERIALS_DB
silicon = material_properties["Si"]
h = substrate_thickness
stack = LayerStack(pdk_name="qpdk")
layers: list[
tuple[str, float, float, str, Literal["conductor", "dielectric", "via"]]
] = [
(
"VACUUM_BOTTOM",
-h - vacuum_thickness,
vacuum_thickness,
"vacuum",
"dielectric",
),
("MB", -h, 0, "qpdk-niobium", "conductor"),
("SUBSTRATE", -h, h, "qpdk-silicon", "dielectric"),
("TSV", -h, h, "qpdk-tsv-lining", "via"),
("M1", 0.0, 0, "qpdk-niobium", "conductor"),
("VACUUM", 0.0, vacuum_thickness, "vacuum", "dielectric"),
]
for name, zmin, thickness, material, layer_type in layers:
stack.layers[name] = Layer(
name=name,
gds_layer=TSV_FEM_LAYERS[name],
zmin=zmin,
zmax=zmin + thickness,
thickness=thickness,
material=material,
layer_type=layer_type,
)
stack.materials = {
"qpdk-silicon": {
"permittivity": silicon["relative_permittivity"],
"loss_tangent": silicon["loss_tangent"],
},
"qpdk-niobium": {"permittivity": 1.0},
"qpdk-tsv-lining": {"conductivity": tsv_conductivity},
"vacuum": MATERIALS_DB["vacuum"].to_dict(),
}
return stack